Instructions to use Wellz1000/hermes-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- HERMES
How to use Wellz1000/hermes-2 with HERMES:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download tools/browser_cdp_tool.py from Wellz1000/hermes-2: direct link, hf CLI and curl.
- Browser
- Download file 27.9 kB
-
https://huggingface.co/Wellz1000/hermes-2/resolve/main/tools/browser_cdp_tool.py
- Command line
-
hf download hf://Wellz1000/hermes-2/tools/browser_cdp_tool.py
-
curl -L -o browser_cdp_tool.py https://huggingface.co/Wellz1000/hermes-2/resolve/main/tools/browser_cdp_tool.py
27.9 kB
| #!/usr/bin/env python3 | |
| """ | |
| Raw Chrome DevTools Protocol (CDP) passthrough tool. | |
| Exposes a single tool, ``browser_cdp``, that sends arbitrary CDP commands to | |
| the browser's DevTools WebSocket endpoint. Works when a CDP URL is | |
| configured β either via ``/browser connect`` (sets ``BROWSER_CDP_URL``) or | |
| ``browser.cdp_url`` in ``config.yaml`` β or when a CDP-backed cloud provider | |
| session is active. | |
| This is the escape hatch for browser operations not covered by the main | |
| browser tool surface (``browser_navigate``, ``browser_click``, | |
| ``browser_console``, etc.) β handling native dialogs, iframe-scoped | |
| evaluation, cookie/network control, low-level tab management, etc. | |
| Method reference: https://chromedevtools.github.io/devtools-protocol/ | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import logging | |
| from typing import Any, Dict, Optional | |
| from tools.registry import registry, tool_error | |
| logger = logging.getLogger(__name__) | |
| CDP_DOCS_URL = "https://chromedevtools.github.io/devtools-protocol/" | |
| _CDP_PRIVATE_PAGE_ALLOWED_METHODS = { | |
| # Browser/target inspection does not read the current page body, cookies, | |
| # DOM, storage, or screenshots. Keep these working so the model can list | |
| # tabs or navigate away from a blocked page. | |
| "Browser.getVersion", | |
| "Target.getTargets", | |
| "Target.attachToTarget", | |
| "Target.detachFromTarget", | |
| "Page.navigate", | |
| "Page.reload", | |
| "Page.stopLoading", | |
| } | |
| def _redact_cdp_output(value: Any) -> Any: | |
| """Redact browser-originated CDP result data before returning it.""" | |
| from agent.redact import redact_sensitive_text | |
| if isinstance(value, str): | |
| return redact_sensitive_text(value, force=True) | |
| if isinstance(value, list): | |
| return [_redact_cdp_output(item) for item in value] | |
| if isinstance(value, tuple): | |
| return tuple(_redact_cdp_output(item) for item in value) | |
| if isinstance(value, dict): | |
| return {key: _redact_cdp_output(item) for key, item in value.items()} | |
| return value | |
| # ``websockets`` is a direct hermes-agent dependency because the browser CDP | |
| # supervisor and browser_dialog tool import it during tool discovery. Wrap the | |
| # import so a clean error surfaces if an environment is stale or incomplete. | |
| try: | |
| import websockets | |
| from websockets.exceptions import WebSocketException | |
| _WS_AVAILABLE = True | |
| except ImportError: | |
| websockets = None # type: ignore[assignment] | |
| WebSocketException = Exception # type: ignore[assignment,misc] | |
| _WS_AVAILABLE = False | |
| # --------------------------------------------------------------------------- | |
| # Async-from-sync bridge (matches the pattern in homeassistant_tool.py) | |
| # --------------------------------------------------------------------------- | |
| def _run_async(coro): | |
| """Run an async coroutine from a sync handler, safe inside or outside a loop.""" | |
| try: | |
| loop = asyncio.get_running_loop() | |
| except RuntimeError: | |
| loop = None | |
| if loop and loop.is_running(): | |
| import concurrent.futures | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | |
| future = pool.submit(asyncio.run, coro) | |
| return future.result() | |
| return asyncio.run(coro) | |
| # --------------------------------------------------------------------------- | |
| # Endpoint resolution | |
| # --------------------------------------------------------------------------- | |
| def _resolve_cdp_endpoint() -> str: | |
| """Return the normalized CDP WebSocket URL, or empty string if unavailable. | |
| Delegates to ``tools.browser_tool._get_cdp_override`` so precedence stays | |
| consistent with the rest of the browser tool surface: | |
| 1. ``BROWSER_CDP_URL`` env var (live override from ``/browser connect``) | |
| 2. ``browser.cdp_url`` in ``config.yaml`` | |
| """ | |
| try: | |
| from tools.browser_tool import _get_cdp_override # type: ignore[import-not-found] | |
| return (_get_cdp_override() or "").strip() | |
| except Exception as exc: # pragma: no cover β defensive | |
| logger.debug("browser_cdp: failed to resolve CDP endpoint: %s", exc) | |
| return "" | |
| def _private_page_guard_error(blocked_url: str, method: str) -> str: | |
| return tool_error( | |
| "Blocked: page URL targets a private or internal address " | |
| f"({blocked_url}). Raw CDP method {method!r} could expose private " | |
| "page content or state.", | |
| method=method, | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| def _browser_cdp_private_guard( | |
| *, | |
| task_id: str, | |
| method: str, | |
| params: Dict[str, Any], | |
| ) -> Optional[str]: | |
| """Apply the browser SSRF/private-page guard to raw CDP calls. | |
| ``browser_cdp`` is intentionally an escape hatch, but it still shares the | |
| same cloud/private-network boundary as ``browser_snapshot``, | |
| ``browser_console`` and ``browser_eval``. If a cloud browser has landed on | |
| a private/internal URL (for example via a prior eval navigation), raw CDP | |
| calls like ``Runtime.evaluate`` or ``DOM.getDocument`` must not become the | |
| sibling bypass for the guarded browser tools. | |
| """ | |
| try: | |
| from tools import browser_tool as bt # type: ignore[import-not-found] | |
| if not bt._eval_ssrf_guard_active(task_id): # type: ignore[attr-defined] | |
| return None | |
| if method == "Page.navigate": | |
| target_url = str((params or {}).get("url") or "").strip() | |
| if target_url and ( | |
| bt._is_always_blocked_url(target_url) # type: ignore[attr-defined] | |
| or not bt._is_safe_url(target_url) # type: ignore[attr-defined] | |
| ): | |
| return tool_error( | |
| "Blocked: CDP Page.navigate target is a private or " | |
| f"internal address ({target_url}).", | |
| method=method, | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| if method == "Runtime.evaluate": | |
| expression = str((params or {}).get("expression") or "") | |
| blocked_literal = bt._expression_targets_private_url(expression) # type: ignore[attr-defined] | |
| if blocked_literal: | |
| return tool_error( | |
| "Blocked: CDP Runtime.evaluate expression targets a " | |
| f"private or internal address ({blocked_literal}).", | |
| method=method, | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| if method not in _CDP_PRIVATE_PAGE_ALLOWED_METHODS: | |
| blocked_url = bt._current_page_private_url(task_id) # type: ignore[attr-defined] | |
| if blocked_url: | |
| return _private_page_guard_error(blocked_url, method) | |
| except Exception as exc: # noqa: BLE001 | |
| # Match the existing browser guards' posture: guard probes are | |
| # best-effort and should not break local/custom CDP workflows. | |
| logger.debug("browser_cdp: private-page guard probe failed: %s", exc) | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Core CDP call | |
| # --------------------------------------------------------------------------- | |
| async def _cdp_call( | |
| ws_url: str, | |
| method: str, | |
| params: Dict[str, Any], | |
| target_id: Optional[str], | |
| timeout: float, | |
| ) -> Dict[str, Any]: | |
| """Make a single CDP call, optionally attaching to a target first. | |
| When ``target_id`` is provided, we call ``Target.attachToTarget`` with | |
| ``flatten=True`` to multiplex a page-level session over the same | |
| browser-level WebSocket, then send ``method`` with that ``sessionId``. | |
| When ``target_id`` is None, ``method`` is sent at browser level β which | |
| works for ``Target.*``, ``Browser.*``, ``Storage.*`` and a few other | |
| globally-scoped domains. | |
| """ | |
| assert websockets is not None # guarded by _WS_AVAILABLE at call-site | |
| async with websockets.connect( | |
| ws_url, | |
| max_size=None, # CDP responses (e.g. DOM.getDocument) can be large | |
| open_timeout=timeout, | |
| close_timeout=5, | |
| ping_interval=None, # CDP server doesn't expect pings | |
| ) as ws: | |
| next_id = 1 | |
| session_id: Optional[str] = None | |
| # --- Step 1: attach to target if requested --- | |
| if target_id: | |
| attach_id = next_id | |
| next_id += 1 | |
| await ws.send( | |
| json.dumps( | |
| { | |
| "id": attach_id, | |
| "method": "Target.attachToTarget", | |
| "params": {"targetId": target_id, "flatten": True}, | |
| } | |
| ) | |
| ) | |
| deadline = asyncio.get_running_loop().time() + timeout | |
| while True: | |
| remaining = deadline - asyncio.get_running_loop().time() | |
| if remaining <= 0: | |
| raise TimeoutError( | |
| f"Timed out attaching to target {target_id}" | |
| ) | |
| raw = await asyncio.wait_for(ws.recv(), timeout=remaining) | |
| msg = json.loads(raw) | |
| if msg.get("id") == attach_id: | |
| if "error" in msg: | |
| raise RuntimeError( | |
| f"Target.attachToTarget failed: {msg['error']}" | |
| ) | |
| session_id = msg.get("result", {}).get("sessionId") | |
| if not session_id: | |
| raise RuntimeError( | |
| "Target.attachToTarget did not return a sessionId" | |
| ) | |
| break | |
| # Ignore events (messages without "id") while waiting | |
| # --- Step 2: dispatch the real method --- | |
| call_id = next_id | |
| next_id += 1 | |
| req: Dict[str, Any] = { | |
| "id": call_id, | |
| "method": method, | |
| "params": params or {}, | |
| } | |
| if session_id: | |
| req["sessionId"] = session_id | |
| await ws.send(json.dumps(req)) | |
| deadline = asyncio.get_running_loop().time() + timeout | |
| while True: | |
| remaining = deadline - asyncio.get_running_loop().time() | |
| if remaining <= 0: | |
| raise TimeoutError( | |
| f"Timed out waiting for response to {method}" | |
| ) | |
| raw = await asyncio.wait_for(ws.recv(), timeout=remaining) | |
| msg = json.loads(raw) | |
| if msg.get("id") == call_id: | |
| if "error" in msg: | |
| raise RuntimeError(f"CDP error: {msg['error']}") | |
| return msg.get("result", {}) | |
| # Ignore events / out-of-order responses | |
| # --------------------------------------------------------------------------- | |
| # Public tool function | |
| # --------------------------------------------------------------------------- | |
| def _browser_cdp_via_supervisor( | |
| task_id: str, | |
| frame_id: str, | |
| method: str, | |
| params: Optional[Dict[str, Any]], | |
| timeout: float, | |
| ) -> str: | |
| """Route a CDP call through the live supervisor session for an OOPIF frame. | |
| Looks up the frame in the supervisor's snapshot, extracts its child | |
| ``cdp_session_id``, and dispatches ``method`` with that sessionId via | |
| the supervisor's already-connected WebSocket (using | |
| ``asyncio.run_coroutine_threadsafe`` onto the supervisor loop). | |
| """ | |
| try: | |
| from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found] | |
| except Exception as exc: # pragma: no cover β defensive | |
| return tool_error( | |
| f"CDP supervisor is not available: {exc}. frame_id routing requires " | |
| f"a running supervisor attached via /browser connect or an active " | |
| f"Browserbase session." | |
| ) | |
| supervisor = SUPERVISOR_REGISTRY.get(task_id) | |
| if supervisor is None: | |
| return tool_error( | |
| f"No CDP supervisor is attached for task={task_id!r}. Call " | |
| f"browser_navigate or /browser connect first so the supervisor " | |
| f"can attach. Once attached, browser_snapshot will populate " | |
| f"frame_tree with frame_ids you can pass here." | |
| ) | |
| snap = supervisor.snapshot() | |
| # Search both the top frame and the children for the requested id. | |
| top = snap.frame_tree.get("top") | |
| frame_info: Optional[Dict[str, Any]] = None | |
| if top and top.get("frame_id") == frame_id: | |
| frame_info = top | |
| else: | |
| for child in snap.frame_tree.get("children", []) or []: | |
| if child.get("frame_id") == frame_id: | |
| frame_info = child | |
| break | |
| if frame_info is None: | |
| # Check the raw frames dict too (frame_tree is capped at 30 entries) | |
| with supervisor._state_lock: # type: ignore[attr-defined] | |
| raw = supervisor._frames.get(frame_id) # type: ignore[attr-defined] | |
| if raw is not None: | |
| frame_info = raw.to_dict() | |
| if frame_info is None: | |
| return tool_error( | |
| f"frame_id {frame_id!r} not found in supervisor state. " | |
| f"Call browser_snapshot to see current frame_tree." | |
| ) | |
| child_sid = frame_info.get("session_id") | |
| if not child_sid: | |
| # Not an OOPIF β fall back to top-level session (evaluating at page | |
| # scope). Same-origin iframes don't get their own sessionId; the | |
| # agent can still use contentWindow/contentDocument from the parent. | |
| return tool_error( | |
| f"frame_id {frame_id!r} is not an out-of-process iframe (no " | |
| f"dedicated CDP session). For same-origin iframes, use " | |
| f"`browser_cdp(method='Runtime.evaluate', params={{'expression': " | |
| f"\"document.querySelector('iframe').contentDocument.title\"}})` " | |
| f"at the top-level page instead." | |
| ) | |
| # Dispatch onto the supervisor's loop. | |
| loop = supervisor._loop # type: ignore[attr-defined] | |
| if loop is None or not loop.is_running(): | |
| return tool_error( | |
| "CDP supervisor loop is not running. Try reconnecting with " | |
| "/browser connect." | |
| ) | |
| async def _do_cdp(): | |
| return await supervisor._cdp( # type: ignore[attr-defined] | |
| method, | |
| params or {}, | |
| session_id=child_sid, | |
| timeout=timeout, | |
| ) | |
| try: | |
| from agent.async_utils import safe_schedule_threadsafe | |
| fut = safe_schedule_threadsafe(_do_cdp(), loop) | |
| if fut is None: | |
| return tool_error( | |
| "CDP call via supervisor failed: loop unavailable", | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| result_msg = fut.result(timeout=timeout + 2) | |
| except Exception as exc: | |
| return tool_error( | |
| f"CDP call via supervisor failed: {type(exc).__name__}: {exc}", | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| payload: Dict[str, Any] = { | |
| "success": True, | |
| "method": method, | |
| "frame_id": frame_id, | |
| "session_id": child_sid, | |
| "result": result_msg.get("result", {}), | |
| } | |
| return json.dumps(payload, ensure_ascii=False) | |
| def browser_cdp( | |
| method: str, | |
| params: Optional[Dict[str, Any]] = None, | |
| target_id: Optional[str] = None, | |
| frame_id: Optional[str] = None, | |
| timeout: float = 30.0, | |
| task_id: Optional[str] = None, | |
| ) -> str: | |
| """Send a raw CDP command. See ``CDP_DOCS_URL`` for method documentation. | |
| Args: | |
| method: CDP method name, e.g. ``"Target.getTargets"``. | |
| params: Method-specific parameters; defaults to ``{}``. | |
| target_id: Optional target/tab ID for page-level methods. When set, | |
| we first attach to the target (``flatten=True``) and send | |
| ``method`` with the resulting ``sessionId``. Uses a fresh | |
| stateless CDP connection. | |
| frame_id: Optional cross-origin (OOPIF) iframe ``frame_id`` from | |
| ``browser_snapshot.frame_tree.children[]``. When set (and the | |
| frame is an OOPIF with a live session tracked by the CDP | |
| supervisor), routes the call through the supervisor's existing | |
| WebSocket β which is how you Runtime.evaluate *inside* an | |
| iframe on backends where per-call fresh CDP connections would | |
| hit signed-URL expiry (Browserbase) or expensive reattach. | |
| timeout: Seconds to wait for the call to complete. | |
| task_id: Task identifier for supervisor lookup. When ``frame_id`` | |
| is set, this identifies which task's supervisor to use; the | |
| handler will default to ``"default"`` otherwise. | |
| Returns: | |
| JSON string ``{"success": True, "method": ..., "result": {...}}`` on | |
| success, or ``{"error": "..."}`` on failure. | |
| """ | |
| effective_task_id = task_id or "default" | |
| # --- Route iframe-scoped calls through the supervisor --------------- | |
| if frame_id: | |
| # Same private-page/SSRF boundary as the stateless path below β | |
| # frame_id routing must not become the sibling bypass for it. | |
| blocked = _browser_cdp_private_guard( | |
| task_id=effective_task_id, | |
| method=method, | |
| params=params or {}, | |
| ) | |
| if blocked: | |
| return blocked | |
| return _browser_cdp_via_supervisor( | |
| task_id=effective_task_id, | |
| frame_id=frame_id, | |
| method=method, | |
| params=params, | |
| timeout=timeout, | |
| ) | |
| if not method or not isinstance(method, str): | |
| return tool_error( | |
| "'method' is required (e.g. 'Target.getTargets')", | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| if not _WS_AVAILABLE: | |
| return tool_error( | |
| "The 'websockets' Python package is required but not installed. " | |
| "Install it with: pip install websockets" | |
| ) | |
| endpoint = _resolve_cdp_endpoint() | |
| if not endpoint: | |
| return tool_error( | |
| "No CDP endpoint is available. Run '/browser connect' to attach " | |
| "to a running Chrome, Brave, Chromium, or Edge browser, or set " | |
| "'browser.cdp_url' in config.yaml. The Camofox backend is REST-only " | |
| "and does not expose CDP.", | |
| cdp_docs=CDP_DOCS_URL, | |
| ) | |
| if not endpoint.startswith(("ws://", "wss://")): | |
| return tool_error( | |
| f"CDP endpoint is not a WebSocket URL: {endpoint!r}. " | |
| "Expected ws://... or wss://... β the /browser connect " | |
| "resolver should have rewritten this. Check that a Chromium-family " | |
| "browser is actually listening on the debug port." | |
| ) | |
| call_params: Dict[str, Any] = params or {} | |
| if not isinstance(call_params, dict): | |
| return tool_error( | |
| f"'params' must be an object/dict, got {type(call_params).__name__}" | |
| ) | |
| blocked = _browser_cdp_private_guard( | |
| task_id=effective_task_id, | |
| method=method, | |
| params=call_params, | |
| ) | |
| if blocked: | |
| return blocked | |
| try: | |
| safe_timeout = float(timeout) if timeout else 30.0 | |
| except (TypeError, ValueError): | |
| safe_timeout = 30.0 | |
| safe_timeout = max(1.0, min(safe_timeout, 300.0)) | |
| try: | |
| result = _run_async( | |
| _cdp_call(endpoint, method, call_params, target_id, safe_timeout) | |
| ) | |
| except asyncio.TimeoutError as exc: | |
| return tool_error( | |
| f"CDP call timed out after {safe_timeout}s: {exc}", | |
| method=method, | |
| ) | |
| except TimeoutError as exc: | |
| return tool_error(str(exc), method=method) | |
| except RuntimeError as exc: | |
| return tool_error(str(exc), method=method) | |
| except WebSocketException as exc: | |
| return tool_error( | |
| f"WebSocket error talking to CDP at {endpoint}: {exc}. The " | |
| "browser may have disconnected β try '/browser connect' again.", | |
| method=method, | |
| ) | |
| except Exception as exc: # pragma: no cover β unexpected | |
| logger.exception("browser_cdp unexpected error") | |
| return tool_error( | |
| f"Unexpected error: {type(exc).__name__}: {exc}", | |
| method=method, | |
| ) | |
| payload: Dict[str, Any] = { | |
| "success": True, | |
| "method": method, | |
| "result": _redact_cdp_output(result), | |
| } | |
| if target_id: | |
| payload["target_id"] = target_id | |
| return json.dumps(payload, ensure_ascii=False) | |
| # --------------------------------------------------------------------------- | |
| # Registry | |
| # --------------------------------------------------------------------------- | |
| BROWSER_CDP_SCHEMA: Dict[str, Any] = { | |
| "name": "browser_cdp", | |
| "description": ( | |
| "Send a raw Chrome DevTools Protocol (CDP) command. Escape hatch for " | |
| "browser operations not covered by browser_navigate, browser_click, " | |
| "browser_console, etc.\n\n" | |
| "**Requires a reachable CDP endpoint.** Available when the user has " | |
| "run '/browser connect' to attach to a running Chrome, Brave, Chromium, " | |
| "or Edge browser, or when 'browser.cdp_url' is set in config.yaml. " | |
| "Not currently wired up for cloud backends (Browserbase, Browser Use, " | |
| "Firecrawl) β those expose CDP per session but live-session routing is " | |
| "a follow-up. Camofox is REST-only and will never support CDP. If the " | |
| "tool is in your toolset at all, a CDP endpoint is already reachable.\n\n" | |
| f"**CDP method reference:** {CDP_DOCS_URL} β use web_extract on a " | |
| "method's URL (e.g. '/tot/Page/#method-handleJavaScriptDialog') " | |
| "to look up parameters and return shape.\n\n" | |
| "**Common patterns:**\n" | |
| "- List tabs: method='Target.getTargets', params={}\n" | |
| "- Handle a native JS dialog: method='Page.handleJavaScriptDialog', " | |
| "params={'accept': true, 'promptText': ''}, target_id=<tabId>\n" | |
| "- Get all cookies: method='Network.getAllCookies', params={}\n" | |
| "- Eval in a specific tab: method='Runtime.evaluate', " | |
| "params={'expression': '...', 'returnByValue': true}, " | |
| "target_id=<tabId>\n" | |
| "- Set viewport for a tab: method='Emulation.setDeviceMetricsOverride', " | |
| "params={'width': 1280, 'height': 720, 'deviceScaleFactor': 1, " | |
| "'mobile': false}, target_id=<tabId>\n\n" | |
| "**Usage rules:**\n" | |
| "- Browser-level methods (Target.*, Browser.*, Storage.*): omit " | |
| "target_id and frame_id.\n" | |
| "- Page-level methods (Page.*, Runtime.*, DOM.*, Emulation.*, " | |
| "Network.* scoped to a tab): pass target_id from Target.getTargets.\n" | |
| "- **Cross-origin iframe scope** (Runtime.evaluate inside an OOPIF, " | |
| "Page.* targeting a frame target, etc.): pass frame_id from the " | |
| "browser_snapshot frame_tree output. This routes through the CDP " | |
| "supervisor's live connection β the only reliable way on " | |
| "Browserbase where stateless CDP calls hit signed-URL expiry.\n" | |
| "- Each stateless call (without frame_id) is independent β sessions " | |
| "and event subscriptions do not persist between calls. For stateful " | |
| "workflows, prefer the dedicated browser tools or use frame_id " | |
| "routing." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "method": { | |
| "type": "string", | |
| "description": ( | |
| "CDP method name, e.g. 'Target.getTargets', " | |
| "'Runtime.evaluate', 'Page.handleJavaScriptDialog'." | |
| ), | |
| }, | |
| "params": { | |
| "type": "object", | |
| "description": ( | |
| "Method-specific parameters as a JSON object. Omit or " | |
| "pass {} for methods that take no parameters." | |
| ), | |
| "properties": {}, | |
| "additionalProperties": True, | |
| }, | |
| "target_id": { | |
| "type": "string", | |
| "description": ( | |
| "Optional. Target/tab ID from Target.getTargets result " | |
| "(each entry's 'targetId'). Use for page-level methods " | |
| "at the top-level tab scope. Mutually exclusive with " | |
| "frame_id." | |
| ), | |
| }, | |
| "frame_id": { | |
| "type": "string", | |
| "description": ( | |
| "Optional. Out-of-process iframe (OOPIF) frame_id from " | |
| "browser_snapshot.frame_tree.children[] where " | |
| "is_oopif=true. When set, routes the call through the " | |
| "CDP supervisor's live session for that iframe. " | |
| "Essential for Runtime.evaluate inside cross-origin " | |
| "iframes, especially on Browserbase where fresh " | |
| "per-call CDP connections can't keep up with signed " | |
| "URL rotation. For same-origin iframes, use parent " | |
| "contentWindow/contentDocument from Runtime.evaluate " | |
| "at the top-level page instead." | |
| ), | |
| }, | |
| "timeout": { | |
| "type": "number", | |
| "description": ( | |
| "Timeout in seconds (default 30, max 300)." | |
| ), | |
| "default": 30, | |
| }, | |
| }, | |
| "required": ["method"], | |
| }, | |
| } | |
| def _browser_cdp_check() -> bool: | |
| """Availability check for browser_cdp. | |
| The tool is only offered when the Python side can actually reach a CDP | |
| endpoint right now β meaning a static URL is set via ``/browser connect`` | |
| (``BROWSER_CDP_URL``) or ``browser.cdp_url`` in ``config.yaml``. | |
| Backends that do *not* currently expose CDP to us β Camofox (REST-only), | |
| the default local agent-browser mode (Playwright hides its internal CDP | |
| port), and cloud providers whose per-session ``cdp_url`` is not yet | |
| surfaced β are gated out so the model doesn't see a tool that would | |
| reliably fail. Cloud-provider CDP routing is a follow-up. | |
| Kept in a thin wrapper so the registration statement stays at module top | |
| level (the tool-discovery AST scan only picks up top-level | |
| ``registry.register(...)`` calls). | |
| """ | |
| try: | |
| from tools.browser_tool import ( # type: ignore[import-not-found] | |
| _get_cdp_override_raw, | |
| check_browser_requirements, | |
| ) | |
| except ImportError as exc: # pragma: no cover β defensive | |
| logger.debug("browser_cdp check: browser_tool import failed: %s", exc) | |
| return False | |
| if not check_browser_requirements(): | |
| return False | |
| # Raw (no-I/O) gate: check_fns run during tool-schema assembly at every | |
| # startup; resolving the endpoint over HTTP here would block launch when | |
| # the configured endpoint is stale/unreachable. | |
| return bool(_get_cdp_override_raw()) | |
| registry.register( | |
| name="browser_cdp", | |
| toolset="browser-cdp", | |
| schema=BROWSER_CDP_SCHEMA, | |
| handler=lambda args, **kw: browser_cdp( | |
| method=args.get("method", ""), | |
| params=args.get("params"), | |
| target_id=args.get("target_id"), | |
| frame_id=args.get("frame_id"), | |
| timeout=args.get("timeout", 30.0), | |
| task_id=kw.get("task_id"), | |
| ), | |
| check_fn=_browser_cdp_check, | |
| emoji="π§ͺ", | |
| ) | |