""" tools/browser_tool.py — Playwright headless browser tool. Read operations (fetch_page, screenshot) never require confirmation. Interactive operations (click, fill, select, press) require confirmation unless the target domain is in the user's browser_approved_domains list. Sessions are stateful within a session_id: navigate with fetch_page first, then use interactive ops without a url to act on the current page. Requires: playwright package + `playwright install chromium` """ from __future__ import annotations import asyncio import logging from typing import ClassVar from ..context_vars import current_task_id, current_session_id, web_tier2_enabled from ..security import assert_domain_tier1, sanitize_external_content from .base import BaseTool, ToolResult logger = logging.getLogger(__name__) _MAX_TEXT_CHARS = 25_000 _TIMEOUT_MS = 30_000 _USER_AGENT = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ) _INTERACTIVE_OPS = {"click", "fill", "select", "press"} async def _is_domain_approved(user_id: str, hostname: str) -> bool: """Return True if hostname (or a parent domain) is in the user's approved list.""" from ..database import get_pool pool = await get_pool() rows = await pool.fetch( "SELECT domain FROM browser_approved_domains WHERE owner_user_id = $1", user_id, ) hostname = hostname.lower() for row in rows: d = row["domain"].lower().lstrip("*.") if hostname == d or hostname.endswith("." + d): return True return False class BrowserTool(BaseTool): name = "browser" description = ( "Headless Chromium browser for JS-heavy pages and web interactions. " "Read ops: fetch_page (extract text), screenshot (PNG). " "Interactive ops: click, fill (type into field), select (dropdown), press (keyboard key). " "Interactive ops require confirmation unless the domain is in your Browser Trusted Domains list. " "Page state is kept across calls within the same session — navigate with fetch_page first, " "then use interactive ops (omit url to stay on the current page). " "Follows the same domain whitelist rules as the web tool." ) input_schema = { "type": "object", "properties": { "operation": { "type": "string", "enum": ["fetch_page", "screenshot", "click", "fill", "select", "press"], "description": ( "fetch_page: extract page text. " "screenshot: capture PNG. " "click: click an element (selector required). " "fill: type into a field (selector + value required). " "select: choose a