Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bobmcwilliams4_shadowglass-browser.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
62 KiB
JSON

{"repo": "bobmcwilliams4/shadowglass-browser", "files": {"browser-use-bridge.py": {"57790f69fd19303babeee39a3c3a970178624b74": {"t": 1786068484.7806592, "n": 59584, "h": "3f33708b44819cff", "c": "\"\"\"\nShadowGlass Browser-Use Bridge v2.0.0\n=====================================\nFastAPI service bridging AI agent capabilities to ShadowGlass Browser.\n\nQUAD OPUS 4.6 ARCHITECTURE:\n - 4x Claude Code CLI (claude -p) subprocesses as primary AI brain\n - DeepSeek V3 via browser-use as fallback\n - Task queue distributes work across the 4 Opus instances\n - Each instance gets browser context (DOM/screenshots) and returns actions\n\nConnects to ShadowGlass via:\n 1. CDP (Chrome DevTools Protocol) on ws://127.0.0.1:9242\n 2. HTTP API on http://127.0.0.1:9476\n\nCommander: Bobby Don McWilliams II | ECHO OMEGA PRIME\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport subprocess\nimport sys\nimport time\nimport uuid\nfrom contextlib import asynccontextmanager\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Any, Optional\n\nimport httpx\nfrom fastapi import FastAPI, HTTPException, BackgroundTasks\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom pydantic import BaseModel, Field\nfrom loguru import logger\n\n# ---------------------------------------------------------------------------\n# Configuration\n# ---------------------------------------------------------------------------\n\nCDP_PORT = 9242\nCDP_URL = f\"ws://127.0.0.1:{CDP_PORT}\"\nSHADOWGLASS_API = \"http://127.0.0.1:9476\"\nBRIDGE_PORT = 9243\nBRIDGE_HOST = \"127.0.0.1\"\n\n# Quad Opus 4.6 config\nOPUS_POOL_SIZE = 4\nOPUS_MODEL = \"claude-opus-4-6\"\nOPUS_MAX_TURNS = 25\nOPUS_MAX_BUDGET = 10.0 # USD per task\n\n# Claude CLI path — resolve from multiple possible locations\ndef _find_claude_cli() -> str:\n \"\"\"Find the claude CLI executable path.\"\"\"\n candidates = [\n Path(os.environ.get(\"APPDATA\", \"\")) / \"npm\" / \"claude.cmd\",\n Path(os.environ.get(\"LOCALAPPDATA\", \"\")) / \"npm\" / \"claude.cmd\",\n Path.home() / \"AppData\" / \"Roaming\" / \"npm\" / \"claude.cmd\",\n ]\n for c in candidates:\n if c.exists():\n return str(c)\n # Fallback to bare name (relies on PATH)\n return \"claude\"\n\nCLAUDE_CLI = _find_claude_cli()\n\n# DeepSeek fallback config (browser-use agent)\nDEEPSEEK_ENDPOINT = \"https://api.deepseek.com/v1\"\nDEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"\")\nDEEPSEEK_MODEL = \"deepseek-chat\"\n\n# GitHub Models for lightweight extraction\nGITHUB_ENDPOINT = os.environ.get(\"AZURE_OPENAI_ENDPOINT\", \"https://models.github.ai/inference\")\nGITHUB_API_KEY = os.environ.get(\"AZURE_API_KEY\", os.environ.get(\"GITHUB_TOKEN\", \"\"))\nEXTRACT_MODEL = \"gpt-4.1\"\n\n\n# ---------------------------------------------------------------------------\n# Pydantic Models\n# ---------------------------------------------------------------------------\n\nclass TaskStatus(str, Enum):\n PENDING = \"pending\"\n RUNNING = \"running\"\n COMPLETED = \"completed\"\n FAILED = \"failed\"\n CANCELLED = \"cancelled\"\n\n\nclass AgentEngine(str, Enum):\n \"\"\"Which AI engine powers the agent task.\"\"\"\n OPUS = \"opus\" # Claude Opus 4.6 via CLI subprocess (primary)\n DEEPSEEK = \"deepseek\" # DeepSeek V3 via browser-use (fallback)\n AUTO = \"auto\" # Try Opus first, fall back to DeepSeek\n\n\nclass AgentTaskRequest(BaseModel):\n \"\"\"Request to run an autonomous browser agent task.\"\"\"\n task: str = Field(..., description=\"Natural language description of the task\")\n url: Optional[str] = Field(None, description=\"Starting URL (uses current page if not set)\")\n max_steps: int = Field(25, ge=1, le=100, description=\"Maximum agent steps\")\n use_vision: bool = Field(True, description=\"Enable screenshot-based visual understanding\")\n extract_schema: Optional[dict] = Field(None, description=\"JSON schema for extraction\")\n engine: AgentEngine = Field(AgentEngine.AUTO, description=\"AI engine: opus, deepseek, or auto\")\n system_prompt: Optional[str] = Field(None, description=\"Custom system prompt override\")\n\n\nclass ExtractionRequest(BaseModel):\n \"\"\"Request to extract structured data from current page.\"\"\"\n schema_def: Optional[dict] = Field(None, description=\"JSON schema for output\")\n prompt: Optional[str] = Field(None, description=\"Extraction instruction\")\n url: Optional[str] = Field(None, description=\"URL to navigate to first\")\n format: str = Field(\"markdown\", description=\"Output format: markdown, json, text\")\n\n\nclass CDPCommandRequest(BaseModel):\n \"\"\"Direct CDP command execution.\"\"\"\n method: str = Field(..., description=\"CDP method (e.g., 'Page.navigate')\")\n params: dict = Field(default_factory=dict, description=\"CDP method parameters\")\n\n\nclass TaskResult(BaseModel):\n \"\"\"Result of an agent task execution.\"\"\"\n task_id: str\n status: TaskStatus\n result: Optional[Any] = None\n steps_taken: int = 0\n duration_seconds: float = 0\n error: Optional[str] = None\n extracted_data: Optional[Any] = None\n engine_used: Optional[str] = None\n worker_id: Optional[str] = None\n\n\nclass BridgeStatus(BaseModel):\n \"\"\"Bridge service status.\"\"\"\n version: str = \"2.0.0\"\n shadowglass_connected: bool = False\n cdp_connected: bool = False\n shadowglass_version: Optional[str] = None\n active_tasks: int = 0\n total_tasks_completed: int = 0\n uptime_seconds: float = 0\n opus_pool: dict = {}\n deepseek_available: bool = False\n\n\n# ---------------------------------------------------------------------------\n# Global State\n# ---------------------------------------------------------------------------\n\n_start_time = time.time()\n_active_tasks: dict[str, TaskResult] = {}\n_task_counter = 0\n_total_completed = 0\n\n\n# ---------------------------------------------------------------------------\n# Opus 4.6 Subprocess Pool\n# ---------------------------------------------------------------------------\n\nclass OpusWorker:\n \"\"\"A single Claude Opus 4.6 CLI subprocess worker.\"\"\"\n\n def __init__(self, worker_id: str):\n self.worker_id = worker_id\n self.busy = False\n self.tasks_completed = 0\n self.total_duration = 0.0\n self.last_error: Optional[str] = None\n self.current_task_id: Optional[str] = None\n\n @property\n def avg_duration(self) -> float:\n if self.tasks_completed == 0:\n return 0.0\n return round(self.total_duration / self.tasks_completed, 1)\n\n def to_dict(self) -> dict:\n return {\n \"worker_id\": self.worker_id,\n \"busy\": self.busy,\n \"tasks_completed\": self.tasks_completed,\n \"avg_duration_seconds\": self.avg_duration,\n \"current_task_id\": self.current_task_id,\n \"last_error\": self.last_error,\n }\n\n\nclass OpusPool:\n \"\"\"Pool of 4 Claude Opus 4.6 CLI subprocess workers.\"\"\"\n\n def __init__(self, pool_size: int = OPUS_POOL_SIZE):\n self.workers = [OpusWorker(f\"opus-{i+1}\") for i in range(pool_size)]\n self._lock = asyncio.Lock()\n self._available = asyncio.Condition()\n self.cli_available = False\n\n async def check_cli(self) -> bool:\n \"\"\"Verify claude CLI is available (must unset CLAUDECODE env var).\"\"\"\n try:\n env = os.environ.copy()\n env.pop(\"CLAUDECODE\", None) # Remove nesting protection\n env.pop(\"CLAUDE_CODE_ENTRYPOINT\", None)\n env.pop(\"ANTHROPIC_API_KEY\", None) # Force OAuth, not stale paid key\n logger.info(f\"Checking Claude CLI at: {CLAUDE_CLI}\")\n proc = await asyncio.create_subprocess_exec(\n CLAUDE_CLI, \"--version\",\n stdout=asyncio.subprocess.PIPE,\n stderr=asyncio.subprocess.PIPE,\n env=env,\n )\n stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)\n version = stdout.decode().strip()\n err = stderr.decode().strip()\n logger.info(f\"Claude CLI stdout: {version!r}, stderr: {err!r}, rc: {proc.returncode}\")\n if version and proc.returncode == 0:\n self.cli_available = True\n logger.info(f\"Claude CLI available: {version}\")\n return True\n except Exception as e:\n logger.warning(f\"Claude CLI not available: {e}\")\n self.cli_available = False\n return False\n\n async def acquire(self) -> Optional[OpusWorker]:\n \"\"\"Get an available worker from the pool. Returns None if all busy.\"\"\"\n async with self._lock:\n for worker in self.workers:\n if not worker.busy:\n worker.busy = True\n return worker\n return None\n\n async def acquire_wait(self, timeout: float = 300) -> Optional[OpusWorker]:\n \"\"\"Wait for a worker to become available.\"\"\"\n deadline = time.time() + timeout\n while time.time() < deadline:\n worker = await self.acquire()\n if worker:\n return worker\n await asyncio.sleep(1)\n return None\n\n def release(self, worker: OpusWorker) -> None:\n \"\"\"Return a worker to the pool.\"\"\"\n worker.busy = False\n worker.current_task_id = None\n\n @property\n def available_count(self) -> int:\n return sum(1 for w in self.workers if not w.busy)\n\n @property\n def busy_count(self) -> int:\n return sum(1 for w in self.workers if w.busy)\n\n def to_dict(self) -> dict:\n return {\n \"pool_size\": len(self.workers),\n \"available\": self.available_count,\n \"busy\": self.busy_count,\n \"cli_available\": self.cli_available,\n \"workers\": [w.to_dict() for w in self.workers],\n }\n\n\n_opus_pool = OpusPool(OPUS_POOL_SIZE)\n\n\n# ---------------------------------------------------------------------------\n# ShadowGlass HTTP Client\n# ---------------------------------------------------------------------------\n\n_SG_GET_ROUTES = {\"status\", \"screenshot\", \"snapshot\", \"page-content\", \"history\", \"bookmarks\", \"settings\"}\n\n\nasync def sg_request(action: str, params: dict | None = None) -> Any:\n \"\"\"Send action to ShadowGlass HTTP API (auto-detects GET vs POST).\"\"\"\n async with httpx.AsyncClient(timeout=30) as client:\n try:\n if action in _SG_GET_ROUTES:\n query_str = \"&\".join(f\"{k}={v}\" for k, v in (params or {}).items() if v is not None)\n url = f\"{SHADOWGLASS_API}/{action}\" + (f\"?{query_str}\" if query_str else \"\")\n resp = await client.get(url)\n else:\n resp = await client.post(\n f\"{SHADOWGLASS_API}/{action}\",\n json=params or {},\n )\n return resp.json()\n except httpx.ConnectError:\n raise HTTPException(503, \"ShadowGlass browser not running on port 9476\")\n except Exception as e:\n raise HTTPException(502, f\"ShadowGlass API error: {e}\")\n\n\nasync def sg_status() -> dict:\n \"\"\"Get ShadowGlass status.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"{SHADOWGLASS_API}/status\")\n return resp.json()\n except Exception:\n return {}\n\n\nasync def check_cdp_available() -> bool:\n \"\"\"Check if CDP endpoint is reachable.\"\"\"\n async with httpx.AsyncClient(timeout=3) as client:\n try:\n resp = await client.get(f\"http://127.0.0.1:{CDP_PORT}/json/version\")\n return resp.status_code == 200\n except Exception:\n return False\n\n\nasync def get_browser_context(include_content: bool = True) -> str:\n \"\"\"Get current browser state as context string for Claude.\"\"\"\n parts = []\n\n # Get page status\n try:\n status = await sg_status()\n if status:\n parts.append(f\"Browser: ShadowGlass v{status.get('version', '?')}, {status.get('tabs', 0)} tabs\")\n except Exception:\n pass\n\n # Get page content\n if include_content:\n try:\n content = await sg_request(\"page-content\", {\"format\": \"markdown\"})\n url = content.get(\"url\", \"unknown\")\n title = content.get(\"title\", \"unknown\")\n text = content.get(\"text\", \"\")\n # Truncate to ~50K chars to leave room for Claude's system prompt\n if len(text) > 50000:\n text = text[:50000] + \"\\n... [truncated]\"\n parts.append(f\"Current URL: {url}\")\n parts.append(f\"Page Title: {title}\")\n parts.append(f\"Page Content:\\n{text}\")\n except Exception as e:\n parts.append(f\"Page content unavailable: {e}\")\n\n return \"\\n\\n\".join(parts)\n\n\n# ---------------------------------------------------------------------------\n# Opus 4.6 — Fast Subprocess Helpers (--tools \"\" = no tool init, --system-prompt = replace default)\n# ---------------------------------------------------------------------------\n\nOPUS_SYSTEM_PROMPT = (\n \"You are a browser analysis agent. You receive web page content and a task. \"\n \"Analyze the content and answer the task directly. Be concise. \"\n \"For data extraction, return structured results. No preamble.\"\n)\n\nOPUS_MULTISTEP_SYSTEM_PROMPT = (\n \"You are a browser automation agent controlling ShadowGlass Browser. \"\n \"You receive page content and must decide ONE action per turn. \"\n \"Respond with exactly one JSON object, nothing else. No markdown fences.\"\n)\n\n_FATAL_PATTERNS = [\n \"credit balance\", \"insufficient_quota\", \"billing\",\n \"rate_limit\", \"unauthorized\", \"authentication\",\n]\n\n\ndef _opus_clean_env() -> dict:\n \"\"\"Clean env for Claude CLI subprocess — force OAuth, no nesting protection.\"\"\"\n env = os.environ.copy()\n for k in (\"CLAUDECODE\", \"CLAUDE_CODE_ENTRYPOINT\", \"ANTHROPIC_API_KEY\"):\n env.pop(k, None)\n return env\n\n\ndef _build_opus_cmd(prompt: str, system_prompt: str, max_turns: int = 1) -> list[str]:\n \"\"\"Build fast Claude CLI command — no tools, custom system prompt.\"\"\"\n return [\n CLAUDE_CLI,\n \"-p\", prompt,\n \"--model\", OPUS_MODEL,\n \"--output-format\", \"json\",\n \"--system-prompt\", system_prompt, # Replace bloated default prompt\n \"--tools\", \"\", # No tool loading = FAST startup\n \"--max-turns\", str(max_turns),\n ]\n\n\ndef _parse_opus_output(stdout: str, stderr: str) -> tuple[str | None, dict | None, str | None]:\n \"\"\"Parse Claude CLI JSON output. Returns (result_text, metadata, fatal_error).\"\"\"\n combined = (stdout + \" \" + stderr).lower()\n if any(p in combined for p in _FATAL_PATTERNS):\n return None, None, f\"opus_fatal: {(stdout or stderr)[:300]}\"\n\n try:\n output = json.loads(stdout)\n result_text = output.get(\"result\", stdout)\n if output.get(\"is_error\") or any(p in str(result_text).lower() for p in _FATAL_PATTERNS):\n return None, None, f\"opus_fatal: {str(result_text)[:300]}\"\n meta = {\n \"usage\": output.get(\"usage\", {}),\n \"session_id\": output.get(\"session_id\", \"\"),\n \"model\": OPUS_MODEL,\n }\n return result_text, meta, None\n except json.JSONDecodeError:\n return stdout, None, None\n\n\n# ---------------------------------------------------------------------------\n# Opus 4.6 Agent — Fast Single-Shot Task\n# ---------------------------------------------------------------------------\n\nasync def run_opus_task(task_id: str, request: AgentTaskRequest, worker: OpusWorker) -> None:\n \"\"\"Execute a task using Claude Opus 4.6 CLI subprocess (fast mode).\"\"\"\n global _total_completed\n task_result = _active_tasks[task_id]\n task_result.status = TaskStatus.RUNNING\n task_result.engine_used = \"opus\"\n task_result.worker_id = worker.worker_id\n worker.current_task_id = task_id\n start = time.time()\n\n try:\n if request.url:\n await sg_request(\"navigate\", {\"url\": request.url})\n await asyncio.sleep(2)\n\n browser_ctx = await get_browser_context(include_content=True)\n sys_prompt = request.system_prompt or OPUS_SYSTEM_PROMPT\n\n full_prompt = f\"{browser_ctx}\\n\\n---\\nTASK: {request.task}\\n---\\n\\nComplete this task based on the browser content above. Return your findings directly.\"\n\n cmd = _build_opus_cmd(full_prompt, sys_prompt, max_turns=1)\n env = _opus_clean_env()\n\n logger.info(f\"[{worker.worker_id}] Opus fast-shot task {task_id}\")\n\n proc = await asyncio.create_subprocess_exec(\n *cmd,\n stdout=asyncio.subprocess.PIPE,\n stderr=asyncio.subprocess.PIPE,\n env=env,\n )\n\n try:\n stdout_raw, stderr_raw = await asyncio.wait_for(proc.communicate(), timeout=120)\n except asyncio.TimeoutError:\n proc.kill()\n raise TimeoutError(\"Opus task timed out after 120s\")\n\n stdout = stdout_raw.decode(\"utf-8\", errors=\"replace\").strip()\n stderr = stderr_raw.decode(\"utf-8\", errors=\"replace\").strip()\n if stderr:\n logger.debug(f\"[{worker.worker_id}] stderr: {stderr[:300]}\")\n\n if proc.returncode != 0:\n combined = (stdout + \" \" + stderr).lower()\n if any(p in combined for p in _FATAL_PATTERNS):\n raise RuntimeError(f\"opus_fatal: {(stdout or stderr)[:300]}\")\n raise RuntimeError(f\"Claude CLI exit {proc.returncode}: {stderr[:300]}\")\n\n result_text, meta, fatal = _parse_opus_output(stdout, stderr)\n if fatal:\n raise RuntimeError(fatal)\n\n task_result.result = result_text\n task_result.extracted_data = meta\n task_result.status = TaskStatus.COMPLETED\n task_result.steps_taken = 1\n _total_completed += 1\n worker.tasks_completed += 1\n\n logger.info(f\"[{worker.worker_id}] Task {task_id} done in {round(time.time() - start, 1)}s\")\n\n except Exception as e:\n task_result.status = TaskStatus.FAILED\n task_result.error = str(e)\n worker.last_error = str(e)\n logger.error(f\"[{worker.worker_id}] Task {task_id} failed: {e}\")\n\n finally:\n task_result.duration_seconds = round(time.time() - start, 2)\n worker.total_duration += task_result.duration_seconds\n _opus_pool.release(worker)\n\n\n# ---------------------------------------------------------------------------\n# Opus Multi-Step Agent — Fast Iterative Loop\n# ---------------------------------------------------------------------------\n\nasync def run_opus_multistep(task_id: str, request: AgentTaskRequest, worker: OpusWorker) -> None:\n \"\"\"Execute a multi-step task using Claude Opus 4.6 with action loop (fast mode).\n\n Claude observes the page, decides one action, bridge executes it via ShadowGlass,\n then Claude observes again. Loops until done or max_steps.\n \"\"\"\n global _total_completed\n task_result = _active_tasks[task_id]\n task_result.status = TaskStatus.RUNNING\n task_result.engine_used = \"opus-multistep\"\n task_result.worker_id = worker.worker_id\n worker.current_task_id = task_id\n start = time.time()\n steps = 0\n action_history: list[dict] = []\n\n try:\n if request.url:\n await sg_request(\"navigate\", {\"url\": request.url})\n await asyncio.sleep(2)\n\n env = _opus_clean_env()\n\n for step in range(request.max_steps):\n steps = step + 1\n\n browser_ctx = await get_browser_context(include_content=True)\n\n # Build action history summary (last 5)\n history_text = \"\"\n if action_history:\n history_lines = [\n f\" Step {ah['step']}: {ah['action']} → {ah.get('result', 'ok')[:200]}\"\n for ah in action_history[-5:]\n ]\n history_text = f\"\\nAction History:\\n\" + \"\\n\".join(history_lines)\n\n prompt = f\"\"\"{browser_ctx}\n{history_text}\n\nTASK: {request.task}\n\nDecide your next action. Respond with EXACTLY ONE JSON object (no markdown, no explanation):\n\nIf the task is COMPLETE:\n{{\"done\": true, \"result\": \"your final answer here\"}}\n\nIf you need to NAVIGATE:\n{{\"action\": \"navigate\", \"url\": \"https://...\"}}\n\nIf you need to CLICK (CSS selector):\n{{\"action\": \"click\", \"selector\": \"css selector\"}}\n\nIf you need to TYPE:\n{{\"action\": \"type\", \"selector\": \"css selector\", \"text\": \"text\"}}\n\nIf you need to SCROLL:\n{{\"action\": \"scroll\", \"direction\": \"down\"}}\n\nIf you need to WAIT:\n{{\"action\": \"wait\", \"seconds\": 2}}\n\nJSON only.\"\"\"\n\n cmd = _build_opus_cmd(prompt, OPUS_MULTISTEP_SYSTEM_PROMPT, max_turns=1)\n\n logger.info(f\"[{worker.worker_id}] Step {steps}/{request.max_steps} for {task_id}\")\n\n proc = await asyncio.create_subprocess_exec(\n *cmd,\n stdout=asyncio.subprocess.PIPE,\n stderr=asyncio.subprocess.PIPE,\n env=env,\n )\n\n try:\n stdout_raw, stderr_raw = await asyncio.wait_for(proc.communicate(), timeout=90)\n except asyncio.TimeoutError:\n proc.kill()\n action_history.append({\"step\": steps, \"action\": \"timeout\", \"result\": \"killed\"})\n continue\n\n stdout = stdout_raw.decode(\"utf-8\", errors=\"replace\").strip()\n stderr = stderr_raw.decode(\"utf-8\", errors=\"replace\").strip()\n\n # Fatal error check — abort entire task\n result_text, _, fatal = _parse_opus_output(stdout, stderr)\n if fatal:\n logger.error(f\"[{worker.worker_id}] Fatal: {fatal[:200]}\")\n task_result.status = TaskStatus.FAILED\n task_result.error = fatal\n task_result.result = stdout or stderr\n break\n\n if result_text is None:\n result_text = stdout\n\n # Extract action JSON from Claude's response\n try:\n if isinstance(result_text, str):\n action_text = result_text.strip()\n if action_text.startswith(\"```\"):\n lines = action_text.split(\"\\n\")\n action_text = \"\\n\".join(lines[1:-1] if lines[-1].strip() == \"```\" else lines[1:])\n action = json.loads(action_text)\n else:\n action = result_text\n except (json.JSONDecodeError, TypeError):\n # Natural language response — treat as final result\n task_result.result = result_text\n task_result.status = TaskStatus.COMPLETED\n break\n\n # Task complete?\n if action.get(\"done\"):\n task_result.result = action.get(\"result\", \"Task completed\")\n task_result.status = TaskStatus.COMPLETED\n action_history.append({\"step\": steps, \"action\": \"done\", \"result\": action.get(\"result\", \"\")[:200]})\n break\n\n # Execute action via ShadowGlass\n action_type = action.get(\"action\", \"unknown\")\n action_result = \"ok\"\n\n try:\n if action_type == \"navigate\":\n await sg_request(\"navigate\", {\"url\": action[\"url\"]})\n await asyncio.sleep(2)\n action_result = f\"navigated to {action['url']}\"\n\n elif action_type == \"click\":\n await sg_request(\"evaluate\", {\n \"code\": f\"document.querySelector('{action['selector']}')?.click()\"\n })\n await asyncio.sleep(1)\n action_result = f\"clicked {action['selector']}\"\n\n elif action_type == \"type\":\n selector = action[\"selector\"]\n text = action[\"text\"]\n await sg_request(\"evaluate\", {\n \"code\": (\n f\"const el = document.querySelector('{selector}');\"\n f\"if(el) {{ el.value = '{text}'; \"\n f\"el.dispatchEvent(new Event('input', {{bubbles:true}})); \"\n f\"el.dispatchEvent(new Event('change', {{bubbles:true}})); }}\"\n )\n })\n await asyncio.sleep(1)\n action_result = f\"typed '{text}' into {selector}\"\n\n elif action_type == \"scroll\":\n direction = action.get(\"direction\", \"down\")\n pixels = 500 if direction == \"down\" else -500\n await sg_request(\"evaluate\", {\"code\": f\"window.scrollBy(0, {pixels})\"})\n await asyncio.sleep(0.5)\n action_result = f\"scrolled {direction}\"\n\n elif action_type == \"wait\":\n wait_time = min(float(action.get(\"seconds\", 2)), 10)\n await asyncio.sleep(wait_time)\n action_result = f\"waited {wait_time}s\"\n\n else:\n action_result = f\"unknown action: {action_type}\"\n\n except Exception as e:\n action_result = f\"error: {e}\"\n\n action_history.append({\"step\": steps, \"action\": action_type, \"result\": action_result})\n logger.debug(f\"[{worker.worker_id}] Step {steps}: {action_type} → {action_result}\")\n\n # Exhausted steps without completing (and not already failed)\n if task_result.status not in (TaskStatus.COMPLETED, TaskStatus.FAILED):\n task_result.status = TaskStatus.COMPLETED\n task_result.result = f\"Reached max steps ({request.max_steps}). Last actions: \" + \\\n \"; \".join(f\"{a['action']}→{a['result'][:50]}\" for a in action_history[-3:])\n\n task_result.steps_taken = steps\n _total_completed += 1\n worker.tasks_completed += 1\n logger.info(f\"[{worker.worker_id}] Multi-step {task_id} done in {steps} steps, {round(time.time() - start, 1)}s\")\n\n except Exception as e:\n task_result.status = TaskStatus.FAILED\n task_result.error = str(e)\n task_result.steps_taken = steps\n worker.last_error = str(e)\n logger.error(f\"[{worker.worker_id}] Multi-step {task_id} failed at step {steps}: {e}\")\n\n finally:\n task_result.duration_seconds = round(time.time() - start, 2)\n worker.total_duration += task_result.duration_seconds\n _opus_pool.release(worker)\n\n\n# ---------------------------------------------------------------------------\n# DeepSeek Fallback Agent (browser-use)\n# ---------------------------------------------------------------------------\n\nasync def run_deepseek_task(task_id: str, request: AgentTaskRequest) -> None:\n \"\"\"Execute task using DeepSeek V3 via browser-use (fallback).\"\"\"\n global _total_completed\n task_result = _active_tasks[task_id]\n task_result.status = TaskStatus.RUNNING\n task_result.engine_used = \"deepseek\"\n start = time.time()\n\n try:\n from browser_use import Agent, BrowserSession\n from browser_use.llm.openai.chat import ChatOpenAI as BUChatOpenAI\n\n # Monkey-patch SecurityWatchdog to allow file:// URLs (Electron renderer)\n from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog\n _original = SecurityWatchdog._is_url_allowed\n def _patched(self, url: str) -> bool:\n if url and url.startswith(\"file://\"):\n return True\n return _original(self, url)\n SecurityWatchdog._is_url_allowed = _patched\n\n if request.url:\n await sg_request(\"navigate\", {\"url\": request.url})\n await asyncio.sleep(2)\n\n llm = BUChatOpenAI(\n model=DEEPSEEK_MODEL,\n api_key=DEEPSEEK_API_KEY,\n base_url=DEEPSEEK_ENDPOINT,\n temperature=0.1,\n max_completion_tokens=4096,\n dont_force_structured_output=True,\n add_schema_to_system_prompt=True,\n )\n\n browser_session = BrowserSession(\n cdp_url=f\"http://127.0.0.1:{CDP_PORT}\",\n disable_security=True,\n keep_alive=True,\n )\n\n agent = Agent(\n task=request.task,\n llm=llm,\n browser=browser_session,\n use_vision=request.use_vision,\n max_failures=3,\n max_actions_per_step=5,\n )\n\n result = await agent.run(max_steps=request.max_steps)\n\n task_result.status = TaskStatus.COMPLETED\n task_result.result = str(result.final_result()) if hasattr(result, \"final_result\") else str(result)\n task_result.steps_taken = result.n_steps() if hasattr(result, \"n_steps\") else 0\n _total_completed += 1\n\n except Exception as e:\n task_result.status = TaskStatus.FAILED\n task_result.error = str(e)\n logger.error(f\"DeepSeek fallback task {task_id} failed: {e}\")\n\n finally:\n task_result.duration_seconds = round(time.time() - start, 2)\n\n\n# ---------------------------------------------------------------------------\n# Task Router — Opus primary, DeepSeek fallback\n# ---------------------------------------------------------------------------\n\nasync def route_agent_task(task_id: str, request: AgentTaskRequest) -> None:\n \"\"\"Route task to best available engine.\"\"\"\n task_result = _active_tasks[task_id]\n engine = request.engine\n\n # AUTO: try Opus first, fall back to DeepSeek\n if engine == AgentEngine.AUTO:\n opus_tried = False\n if _opus_pool.cli_available:\n worker = await _opus_pool.acquire()\n if worker:\n logger.info(f\"Routing task {task_id} to Opus 4.6 ({worker.worker_id})\")\n if request.max_steps > 1:\n await run_opus_multistep(task_id, request, worker)\n else:\n await run_opus_task(task_id, request, worker)\n opus_tried = True\n\n # Check if Opus failed with a fatal error — fall back to DeepSeek\n if task_result.status == TaskStatus.FAILED and task_result.error and \"opus_fatal\" in task_result.error:\n logger.warning(f\"Opus fatal error for {task_id}, falling back to DeepSeek: {task_result.error[:100]}\")\n # Reset task for DeepSeek retry\n task_result.status = TaskStatus.PENDING\n task_result.result = None\n task_result.error = None\n task_result.engine_used = None\n task_result.worker_id = None\n task_result.steps_taken = 0\n opus_tried = False # Allow DeepSeek fallback below\n else:\n return\n else:\n logger.info(f\"All Opus workers busy, falling back to DeepSeek for {task_id}\")\n\n # Fallback to DeepSeek\n if not opus_tried and DEEPSEEK_API_KEY:\n logger.info(f\"Routing task {task_id} to DeepSeek V3 fallback\")\n await run_deepseek_task(task_id, request)\n elif not opus_tried:\n task_result.status = TaskStatus.FAILED\n task_result.error = \"No AI engines available (Opus CLI not found, DeepSeek key missing)\"\n\n elif engine == AgentEngine.OPUS:\n if not _opus_pool.cli_available:\n task_result.status = TaskStatus.FAILED\n task_result.error = \"Claude CLI not available\"\n return\n worker = await _opus_pool.acquire_wait(timeout=120)\n if not worker:\n task_result.status = TaskStatus.FAILED\n task_result.error = \"All Opus workers busy (timeout)\"\n return\n if request.max_steps > 1:\n await run_opus_multistep(task_id, request, worker)\n else:\n await run_opus_task(task_id, request, worker)\n\n elif engine == AgentEngine.DEEPSEEK:\n await run_deepseek_task(task_id, request)\n\n\n# ---------------------------------------------------------------------------\n# Extraction (lightweight, uses GitHub Models GPT-4.1)\n# ---------------------------------------------------------------------------\n\nasync def extract_page_content(request: ExtractionRequest) -> dict:\n \"\"\"Extract content from current page using ShadowGlass + LLM.\"\"\"\n if request.url:\n await sg_request(\"navigate\", {\"url\": request.url})\n await asyncio.sleep(2)\n\n content = await sg_request(\"page-content\", {\"format\": request.format})\n\n if not request.prompt and not request.schema_def:\n return content\n\n try:\n from langchain_openai import ChatOpenAI\n\n llm = ChatOpenAI(\n model=EXTRACT_MODEL,\n api_key=GITHUB_API_KEY,\n base_url=GITHUB_ENDPOINT,\n temperature=0,\n )\n\n page_text = content.get(\"text\", \"\")[:8000]\n\n extraction_prompt = f\"\"\"Extract information from this web page content.\n\nPage URL: {content.get('url', 'unknown')}\nPage Title: {content.get('title', 'unknown')}\n\nContent:\n{page_text}\n\n\"\"\"\n if request.prompt:\n extraction_prompt += f\"Instruction: {request.prompt}\\n\"\n if request.schema_def:\n extraction_prompt += f\"Return data matching this JSON schema: {json.dumps(request.schema_def)}\\n\"\n extraction_prompt += \"\\nRespond with valid JSON only.\"\n\n response = await llm.ainvoke(extraction_prompt)\n extracted = response.content\n\n try:\n return {\"extracted\": json.loads(extracted), \"source\": content.get(\"url\")}\n except json.JSONDecodeError:\n return {\"extracted\": extracted, \"source\": content.get(\"url\"), \"format\": \"text\"}\n\n except Exception as e:\n logger.error(f\"LLM extraction failed: {e}\")\n return {**content, \"extraction_error\": str(e)}\n\n\n# ---------------------------------------------------------------------------\n# CDP Direct Access\n# ---------------------------------------------------------------------------\n\nasync def send_cdp_command(method: str, params: dict | None = None) -> dict:\n \"\"\"Send a CDP command directly to the browser.\"\"\"\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n targets_resp = await client.get(f\"http://127.0.0.1:{CDP_PORT}/json\")\n targets = targets_resp.json()\n\n if not targets:\n raise HTTPException(404, \"No CDP targets found\")\n\n page_target = next(\n (t for t in targets if t.get(\"type\") == \"page\"),\n targets[0]\n )\n\n ws_url = page_target.get(\"webSocketDebuggerUrl\")\n if not ws_url:\n raise HTTPException(500, \"No WebSocket URL for target\")\n\n import websockets\n async with websockets.connect(ws_url) as ws:\n msg = json.dumps({\"id\": 1, \"method\": method, \"params\": params or {}})\n await ws.send(msg)\n response = await asyncio.wait_for(ws.recv(), timeout=10)\n return json.loads(response)\n\n except ImportError:\n raise HTTPException(501, \"Install websockets: pip install websockets\")\n except Exception as e:\n raise HTTPException(502, f\"CDP command failed: {e}\")\n\n\n# ---------------------------------------------------------------------------\n# FastAPI App\n# ---------------------------------------------------------------------------\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n \"\"\"Startup and shutdown lifecycle.\"\"\"\n logger.info(f\"ShadowGlass Browser-Use Bridge v2.0.0 — Quad Opus 4.6\")\n logger.info(f\"CDP: ws://127.0.0.1:{CDP_PORT} | ShadowGlass: {SHADOWGLASS_API}\")\n\n # Verify ShadowGlass\n status = await sg_status()\n if status:\n logger.info(f\"ShadowGlass v{status.get('version', '?')} connected ({status.get('tabs', 0)} tabs)\")\n else:\n logger.warning(\"ShadowGlass not detected — start it first\")\n\n # Check CDP\n cdp_ok = await check_cdp_available()\n if cdp_ok:\n logger.info(f\"CDP available on port {CDP_PORT}\")\n else:\n logger.warning(f\"CDP not available on port {CDP_PORT}\")\n\n # Check Claude CLI\n await _opus_pool.check_cli()\n if _opus_pool.cli_available:\n logger.info(f\"Opus pool: {OPUS_POOL_SIZE} workers ready (claude-opus-4-6)\")\n else:\n logger.warning(\"Claude CLI not available — will use DeepSeek fallback only\")\n\n # Check DeepSeek\n ds_ok = bool(DEEPSEEK_API_KEY)\n if ds_ok:\n logger.info(\"DeepSeek V3 fallback available\")\n else:\n logger.warning(\"DEEPSEEK_API_KEY not set — no fallback available\")\n\n yield\n\n for task_id, task in _active_tasks.items():\n if task.status == TaskStatus.RUNNING:\n task.status = TaskStatus.CANCELLED\n logger.info(\"Bridge shutdown complete\")\n\n\napp = FastAPI(\n title=\"ShadowGlass AI Bridge\",\n version=\"2.0.0\",\n description=\"Quad Opus 4.6 + DeepSeek browser agent bridge for ShadowGlass\",\n lifespan=lifespan,\n)\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n\n# ---------------------------------------------------------------------------\n# Endpoints\n# ---------------------------------------------------------------------------\n\n@app.get(\"/health\")\nasync def health():\n \"\"\"Health check.\"\"\"\n sg = await sg_status()\n cdp = await check_cdp_available()\n return {\n \"status\": \"healthy\" if sg and cdp else \"degraded\",\n \"shadowglass\": bool(sg),\n \"cdp\": cdp,\n \"opus_available\": _opus_pool.cli_available,\n \"opus_workers_free\": _opus_pool.available_count,\n \"deepseek_available\": bool(DEEPSEEK_API_KEY),\n \"uptime\": round(time.time() - _start_time, 1),\n }\n\n\n@app.get(\"/status\", response_model=BridgeStatus)\nasync def bridge_status():\n \"\"\"Full bridge status.\"\"\"\n sg = await sg_status()\n cdp = await check_cdp_available()\n return BridgeStatus(\n shadowglass_connected=bool(sg),\n cdp_connected=cdp,\n shadowglass_version=sg.get(\"version\") if sg else None,\n active_tasks=sum(1 for t in _active_tasks.values() if t.status == TaskStatus.RUNNING),\n total_tasks_completed=_total_completed,\n uptime_seconds=round(time.time() - _start_time, 1),\n opus_pool=_opus_pool.to_dict(),\n deepseek_available=bool(DEEPSEEK_API_KEY),\n )\n\n\n@app.post(\"/agent/task\", response_model=TaskResult)\nasync def create_agent_task(request: AgentTaskRequest, background_tasks: BackgroundTasks):\n \"\"\"\n Launch an autonomous browser agent task.\n\n Engine selection:\n - \"auto\" (default): Opus 4.6 primary, DeepSeek V3 fallback\n - \"opus\": Force Claude Opus 4.6\n - \"deepseek\": Force DeepSeek V3 via browser-use\n\n Tasks run in background — poll /agent/task/{task_id} for status.\n \"\"\"\n global _task_counter\n _task_counter += 1\n task_id = f\"sg-{request.engine.value}-{_task_counter:04d}\"\n\n task_result = TaskResult(task_id=task_id, status=TaskStatus.PENDING)\n _active_tasks[task_id] = task_result\n\n background_tasks.add_task(route_agent_task, task_id, request)\n\n logger.info(f\"Task {task_id} created (engine={request.engine.value}): {request.task[:80]}\")\n return task_result\n\n\n@app.get(\"/agent/task/{task_id}\", response_model=TaskResult)\nasync def get_task_status(task_id: str):\n \"\"\"Get status and result of an agent task.\"\"\"\n if task_id not in _active_tasks:\n raise HTTPException(404, f\"Task {task_id} not found\")\n return _active_tasks[task_id]\n\n\n@app.get(\"/agent/tasks\")\nasync def list_tasks():\n \"\"\"List all agent tasks.\"\"\"\n return {\n \"tasks\": [\n {\n \"task_id\": tid,\n \"status\": t.status,\n \"engine\": t.engine_used,\n \"worker\": t.worker_id,\n \"duration\": t.duration_seconds,\n }\n for tid, t in _active_tasks.items()\n ],\n \"total\": len(_active_tasks),\n \"active\": sum(1 for t in _active_tasks.values() if t.status == TaskStatus.RUNNING),\n }\n\n\n@app.post(\"/agent/cancel/{task_id}\")\nasync def cancel_task(task_id: str):\n \"\"\"Cancel a running agent task.\"\"\"\n if task_id not in _active_tasks:\n raise HTTPException(404, f\"Task {task_id} not found\")\n task = _active_tasks[task_id]\n if task.status == TaskStatus.RUNNING:\n task.status = TaskStatus.CANCELLED\n return {\"cancelled\": True, \"task_id\": task_id}\n return {\"cancelled\": False, \"reason\": f\"Task is {task.status}\", \"task_id\": task_id}\n\n\n@app.get(\"/pool\")\nasync def pool_status():\n \"\"\"Get Opus worker pool status.\"\"\"\n return _opus_pool.to_dict()\n\n\n@app.post(\"/pool/refresh\")\nasync def refresh_pool():\n \"\"\"Re-check Claude CLI availability.\"\"\"\n ok = await _opus_pool.check_cli()\n return {\"cli_available\": ok, \"pool\": _opus_pool.to_dict()}\n\n\n@app.post(\"/extract\")\nasync def extract_data(request: ExtractionRequest):\n \"\"\"Extract structured data from a web page using LLM.\"\"\"\n return await extract_page_content(request)\n\n\n@app.get(\"/page/content\")\nasync def get_page_content(format: str = \"markdown\"):\n \"\"\"Get current page content via ShadowGlass.\"\"\"\n return await sg_request(\"page-content\", {\"format\": format})\n\n\n@app.get(\"/page/snapshot\")\nasync def get_page_snapshot():\n \"\"\"Get accessibility tree snapshot.\"\"\"\n return await sg_request(\"snapshot\", {})\n\n\n@app.get(\"/page/screenshot\")\nasync def get_page_screenshot():\n \"\"\"Capture screenshot.\"\"\"\n return await sg_request(\"screenshot\", {})\n\n\n@app.post(\"/navigate\")\nasync def navigate(url: str):\n \"\"\"Navigate to a URL.\"\"\"\n return await sg_request(\"navigate\", {\"url\": url})\n\n\n@app.post(\"/cdp/command\")\nasync def cdp_command(request: CDPCommandRequest):\n \"\"\"Execute a direct CDP command.\"\"\"\n return await send_cdp_command(request.method, request.params)\n\n\n@app.get(\"/cdp/targets\")\nasync def list_cdp_targets():\n \"\"\"List CDP targets.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"http://127.0.0.1:{CDP_PORT}/json\")\n return resp.json()\n except Exception as e:\n raise HTTPException(503, f\"CDP not available: {e}\")\n\n\n@app.get(\"/cdp/version\")\nasync def cdp_version():\n \"\"\"Get CDP/browser version.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"http://127.0.0.1:{CDP_PORT}/json/version\")\n return resp.json()\n except Exception as e:\n raise HTTPException(503, f\"CDP not available: {e}\")\n\n\n@app.post(\"/evasion/profile\")\nasync def set_evasion_profile(profile: str = \"chrome-win\"):\n \"\"\"Set evasion profile.\"\"\"\n return await sg_request(\"evasion\", {\"profile\": profile, \"action\": \"enable\"})\n\n\n@app.post(\"/evasion/level\")\nasync def set_evasion_level(level: str = \"full\"):\n \"\"\"Set evasion level.\"\"\"\n return await sg_request(\"evasion\", {\"level\": level})\n\n\n@app.get(\"/evasion/status\")\nasync def evasion_status():\n \"\"\"Get evasion status.\"\"\"\n return await sg_request(\"evasion\", {})\n\n\n@app.post(\"/search\")\nasync def search(query: str, engines: Optional[str] = None):\n \"\"\"Run a privacy-preserving search.\"\"\"\n params: dict = {\"query\": query}\n if engines:\n params[\"options\"] = {\"engines\": engines.split(\",\")}\n return await sg_request(\"search\", params)\n\n\n@app.post(\"/scrape\")\nasync def scrape(url: str, selectors: Optional[dict] = None):\n \"\"\"Scrape a web page.\"\"\"\n params: dict = {\"url\": url}\n if selectors:\n params[\"selectors\"] = selectors\n return await sg_request(\"scrape\", params)\n\n\n@app.get(\"/tabs\")\nasync def list_tabs():\n \"\"\"List open tabs.\"\"\"\n return await sg_request(\"tabs\", {\"action\": \"list\"})\n\n\n@app.post(\"/tabs/new\")\nasync def new_tab(url: Optional[str] = None):\n \"\"\"Open a new tab.\"\"\"\n return await sg_request(\"tabs\", {\"action\": \"create\", \"url\": url or \"about:blank\"})\n\n\n@app.post(\"/tor/toggle\")\nasync def toggle_tor(enabled: bool = True):\n \"\"\"Toggle Tor routing.\"\"\"\n if enabled:\n return await sg_request(\"tor:start\", {})\n return await sg_request(\"tor:stop\", {})\n\n\n@app.get(\"/tor/status\")\nasync def tor_status():\n \"\"\"Get Tor status.\"\"\"\n return await sg_request(\"tor\", {\"action\": \"status\"})\n\n\n# ---------------------------------------------------------------------------\n# Enhanced Features — Voice, Security, Brain Memory, Crawl4AI\n# ---------------------------------------------------------------------------\n\nECHO_SPEAK_URL = os.environ.get(\"ECHO_SPEAK_URL\", \"http://localhost:8420\")\nSHARED_BRAIN_URL = \"https://echo-shared-brain.bmcii1976.workers.dev\"\nSHARED_BRAIN_INSTANCE = \"shadowglass_browser\"\nVIRUSTOTAL_API_KEY = os.environ.get(\"VIRUSTOTAL_API_KEY\", \"\")\nWHOISXML_API_KEY = os.environ.get(\"WHOISXML_API_KEY\", \"\")\n\n# In-memory security scan history\n_security_history: list[dict] = []\n_brain_auto_capture = True\n\n\n# ── Voice (Echo Speak v2.0 integration) ──────────────────────────────────\n\nclass VoiceSpeakRequest(BaseModel):\n text: str\n voice: str = \"echo\"\n emotion: str = \"neutral\"\n\nclass VoiceTranscribeRequest(BaseModel):\n audio_base64: str\n language: str = \"en\"\n\n@app.post(\"/voice/speak\")\nasync def voice_speak(req: VoiceSpeakRequest):\n \"\"\"Text-to-speech via Echo Speak v2.0.\"\"\"\n async with httpx.AsyncClient(timeout=30) as client:\n try:\n resp = await client.post(\n f\"{ECHO_SPEAK_URL}/tts\",\n json={\"text\": req.text, \"voice\": req.voice, \"emotion\": req.emotion},\n )\n if resp.status_code == 200:\n import base64\n audio_b64 = base64.b64encode(resp.content).decode()\n return {\"audio_base64\": audio_b64, \"format\": \"wav\", \"voice\": req.voice}\n return {\"error\": f\"Echo Speak returned {resp.status_code}\", \"detail\": resp.text[:200]}\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.post(\"/voice/transcribe\")\nasync def voice_transcribe(req: VoiceTranscribeRequest):\n \"\"\"Speech-to-text via Echo Speak (Whisper).\"\"\"\n import base64\n audio_bytes = base64.b64decode(req.audio_base64)\n async with httpx.AsyncClient(timeout=30) as client:\n try:\n resp = await client.post(\n f\"{ECHO_SPEAK_URL}/stt/transcribe\",\n files={\"audio\": (\"recording.wav\", audio_bytes, \"audio/wav\")},\n data={\"language\": req.language},\n )\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.get(\"/voice/voices\")\nasync def voice_list():\n \"\"\"List available TTS voices.\"\"\"\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.get(f\"{ECHO_SPEAK_URL}/voices\")\n return resp.json()\n except Exception as e:\n return {\"voices\": [\n {\"id\": \"echo\", \"name\": \"Echo (Default)\"},\n {\"id\": \"ryan\", \"name\": \"Ryan\"},\n {\"id\": \"luna\", \"name\": \"Luna\"},\n {\"id\": \"commander\", \"name\": \"Commander\"},\n ], \"error\": str(e)}\n\n@app.get(\"/voice/status\")\nasync def voice_status():\n \"\"\"Check Echo Speak availability.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"{ECHO_SPEAK_URL}/health\")\n data = resp.json()\n return {\"available\": True, **data}\n except Exception:\n return {\"available\": False}\n\n\n# ── Security Scanner ─────────────────────────────────────────────────────\n\nclass SecurityScanRequest(BaseModel):\n url: str\n\n@app.post(\"/security/scan\")\nasync def security_scan(req: SecurityScanRequest):\n \"\"\"Analyze page security: SSL, trackers, cookies, suspicious elements.\"\"\"\n from urllib.parse import urlparse\n parsed = urlparse(req.url)\n issues: list[dict] = []\n score = 0 # 0 = safe, 100 = dangerous\n\n # SSL check\n ssl_ok = parsed.scheme == \"https\"\n if not ssl_ok:\n issues.append({\"type\": \"ssl\", \"severity\": \"high\", \"message\": \"Not using HTTPS\"})\n score += 25\n\n # Get page content for analysis\n try:\n content = await sg_request(\"page-content\", {\"format\": \"text\"})\n page_text = content.get(\"text\", \"\")\n page_url = content.get(\"url\", req.url)\n\n # Check for suspicious patterns\n suspicious_patterns = [\n (\"eval(\", \"Dangerous eval() usage detected\"),\n (\"document.write\", \"document.write usage (potential XSS)\"),\n (\"<iframe\", \"Hidden iframe detected\"),\n (\"crypto.subtle\", \"Cryptographic operations detected\"),\n (\"WebSocket\", \"WebSocket connection detected\"),\n ]\n for pattern, msg in suspicious_patterns:\n if pattern.lower() in page_text.lower():\n issues.append({\"type\": \"script\", \"severity\": \"medium\", \"message\": msg})\n score += 8\n\n # Count external resources\n import re\n external_urls = re.findall(r'https?://[^\\s\"\\'<>]+', page_text)\n unique_domains = set()\n for u in external_urls:\n try:\n d = urlparse(u).netloc\n if d and d != parsed.netloc:\n unique_domains.add(d)\n except Exception:\n pass\n\n tracker_domains = {\"google-analytics.com\", \"googletagmanager.com\", \"facebook.net\",\n \"doubleclick.net\", \"analytics\", \"tracker\", \"pixel\", \"beacon\",\n \"hotjar.com\", \"mixpanel.com\", \"segment.io\", \"amplitude.com\"}\n trackers = [d for d in unique_domains if any(t in d for t in tracker_domains)]\n\n if trackers:\n issues.append({\"type\": \"trackers\", \"severity\": \"low\", \"message\": f\"{len(trackers)} trackers: {', '.join(trackers[:5])}\"})\n score += min(len(trackers) * 3, 15)\n\n except Exception as e:\n issues.append({\"type\": \"analysis\", \"severity\": \"info\", \"message\": f\"Page analysis error: {e}\"})\n\n # Clamp score\n score = min(score, 100)\n\n result = {\n \"url\": req.url,\n \"score\": score,\n \"rating\": \"safe\" if score <= 30 else \"caution\" if score <= 60 else \"danger\",\n \"ssl\": {\"valid\": ssl_ok, \"protocol\": parsed.scheme},\n \"issues\": issues,\n \"trackers\": len(trackers) if 'trackers' in dir() else 0,\n \"external_domains\": len(unique_domains) if 'unique_domains' in dir() else 0,\n \"timestamp\": time.time(),\n }\n\n _security_history.append(result)\n if len(_security_history) > 100:\n _security_history.pop(0)\n\n return result\n\n@app.get(\"/security/history\")\nasync def security_history():\n \"\"\"Get scan history.\"\"\"\n return {\"scans\": _security_history[-20:], \"total\": len(_security_history)}\n\n@app.post(\"/security/virustotal\")\nasync def security_virustotal(url: str):\n \"\"\"Check URL against VirusTotal.\"\"\"\n if not VIRUSTOTAL_API_KEY:\n return {\"error\": \"VIRUSTOTAL_API_KEY not configured\"}\n import base64\n url_id = base64.urlsafe_b64encode(url.encode()).decode().strip(\"=\")\n async with httpx.AsyncClient(timeout=15) as client:\n try:\n resp = await client.get(\n f\"https://www.virustotal.com/api/v3/urls/{url_id}\",\n headers={\"x-apikey\": VIRUSTOTAL_API_KEY},\n )\n if resp.status_code == 200:\n data = resp.json()\n stats = data.get(\"data\", {}).get(\"attributes\", {}).get(\"last_analysis_stats\", {})\n return {\"url\": url, \"stats\": stats, \"safe\": stats.get(\"malicious\", 0) == 0}\n return {\"url\": url, \"error\": f\"VT returned {resp.status_code}\"}\n except Exception as e:\n return {\"url\": url, \"error\": str(e)}\n\n@app.post(\"/security/whois\")\nasync def security_whois(domain: str):\n \"\"\"WHOIS lookup for domain.\"\"\"\n if not WHOISXML_API_KEY:\n return {\"error\": \"WHOISXML_API_KEY not configured\"}\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.get(\n f\"https://www.whoisxmlapi.com/whoisserver/WhoisService\",\n params={\"apiKey\": WHOISXML_API_KEY, \"domainName\": domain, \"outputFormat\": \"JSON\"},\n )\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n\n# ── Brain Memory (Echo Shared Brain) ─────────────────────────────────────\n\nclass BrainStoreRequest(BaseModel):\n content: str\n tags: list[str] = []\n importance: int = 5\n url: Optional[str] = None\n title: Optional[str] = None\n\nclass BrainSearchRequest(BaseModel):\n query: str\n limit: int = 10\n\n@app.post(\"/brain/store\")\nasync def brain_store(req: BrainStoreRequest):\n \"\"\"Store browsing memory in Echo Shared Brain.\"\"\"\n payload = {\n \"instance_id\": SHARED_BRAIN_INSTANCE,\n \"role\": \"assistant\",\n \"content\": req.content,\n \"importance\": req.importance,\n \"tags\": [\"browsing\", *req.tags],\n }\n if req.url:\n payload[\"content\"] = f\"[{req.url}] {req.content}\"\n if req.title:\n payload[\"tags\"].append(f\"page:{req.title[:30]}\")\n\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.post(f\"{SHARED_BRAIN_URL}/ingest\", json=payload)\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.post(\"/brain/search\")\nasync def brain_search(req: BrainSearchRequest):\n \"\"\"Semantic search across browsing memory.\"\"\"\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.post(\n f\"{SHARED_BRAIN_URL}/search\",\n json={\"query\": req.query, \"limit\": req.limit},\n )\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.post(\"/brain/recall\")\nasync def brain_recall(url: str):\n \"\"\"Recall memories associated with a URL.\"\"\"\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.post(\n f\"{SHARED_BRAIN_URL}/search\",\n json={\"query\": f\"browsing {url}\", \"limit\": 5},\n )\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.get(\"/brain/recent\")\nasync def brain_recent(limit: int = 10):\n \"\"\"Get recent browsing memories.\"\"\"\n async with httpx.AsyncClient(timeout=10) as client:\n try:\n resp = await client.post(\n f\"{SHARED_BRAIN_URL}/search\",\n json={\"query\": \"browsing page visit\", \"limit\": limit},\n )\n return resp.json()\n except Exception as e:\n return {\"error\": str(e)}\n\n@app.get(\"/brain/stats\")\nasync def brain_stats():\n \"\"\"Get Shared Brain stats.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"{SHARED_BRAIN_URL}/stats\")\n return {\"connected\": True, **resp.json()}\n except Exception:\n return {\"connected\": False}\n\n@app.get(\"/brain/status\")\nasync def brain_status():\n \"\"\"Check Shared Brain connectivity.\"\"\"\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.get(f\"{SHARED_BRAIN_URL}/health\")\n return {\"connected\": True, \"status\": resp.json()}\n except Exception:\n return {\"connected\": False}\n\n@app.post(\"/brain/auto-capture\")\nasync def brain_auto_capture(url: str, title: str, summary: str = \"\"):\n \"\"\"Auto-capture page visit to Shared Brain (called by Electron on navigation).\"\"\"\n global _brain_auto_capture\n if not _brain_auto_capture:\n return {\"stored\": False, \"reason\": \"auto-capture disabled\"}\n\n content = f\"Page visit: {title}\\nURL: {url}\"\n if summary:\n content += f\"\\nSummary: {summary}\"\n\n payload = {\n \"instance_id\": SHARED_BRAIN_INSTANCE,\n \"role\": \"assistant\",\n \"content\": content,\n \"importance\": 3,\n \"tags\": [\"browsing\", \"auto-capture\", \"page-visit\"],\n }\n async with httpx.AsyncClient(timeout=5) as client:\n try:\n resp = await client.post(f\"{SHARED_BRAIN_URL}/ingest\", json=payload)\n return {\"stored\": True, **resp.json()}\n except Exception as e:\n return {\"stored\": False, \"error\": str(e)}\n\n@app.post(\"/brain/toggle-capture\")\nasync def brain_toggle_capture(enabled: bool = True):\n \"\"\"Toggle auto-capture of page visits.\"\"\"\n global _brain_auto_capture\n _brain_auto_capture = enabled\n return {\"auto_capture\": _brain_auto_capture}\n\n\n# ── Crawl4AI Integration ─────────────────────────────────────────────────\n\nclass CrawlRequest(BaseModel):\n url: Optional[str] = None # None = use current page URL\n extract_links: bool = True\n extract_images: bool = False\n max_length: int = 50000\n\n@app.post(\"/crawl/page\")\nasync def crawl_page(req: CrawlRequest):\n \"\"\"Get LLM-ready markdown from current page using Crawl4AI (if installed) or fallback.\"\"\"\n target_url = req.url\n\n if not target_url:\n # Get current page URL from ShadowGlass\n try:\n content = await sg_request(\"page-content\", {\"format\": \"text\"})\n target_url = content.get(\"url\", \"\")\n except Exception:\n return {\"error\": \"No URL provided and couldn't get current page URL\"}\n\n # Try Crawl4AI first\n try:\n from crawl4ai import AsyncWebCrawler\n async with AsyncWebCrawler() as crawler:\n result = await crawler.arun(url=target_url)\n markdown = result.markdown[:req.max_length] if result.markdown else \"\"\n return {\n \"url\": target_url,\n \"markdown\": markdown,\n \"title\": result.metadata.get(\"title\", \"\") if result.metadata else \"\",\n \"links\": result.links[:50] if req.extract_links and result.links else [],\n \"images\": result.media.get(\"images\", [])[:20] if req.extract_images and result.media else [],\n \"engine\": \"crawl4ai\",\n \"length\": len(markdown),\n }\n except ImportError:\n logger.debug(\"crawl4ai not installed, using ShadowGlass page-content fallback\")\n except Exception as e:\n logger.warning(f\"Crawl4AI failed for {target_url}: {e}\")\n\n # Fallback: use ShadowGlass page-content endpoint\n try:\n if req.url:\n await sg_request(\"navigate\", {\"url\": req.url})\n await asyncio.sleep(2)\n\n content = await sg_request(\"page-content\", {\"format\": \"markdown\"})\n text = content.get(\"text\", content.get(\"content\", \"\"))[:req.max_length]\n return {\n \"url\": target_url,\n \"markdown\": text,\n \"title\": content.get(\"title\", \"\"),\n \"links\": [],\n \"images\": [],\n \"engine\": \"shadowglass-fallback\",\n \"length\": len(text),\n }\n except Exception as e:\n return {\"error\": str(e), \"url\": target_url}\n\n@app.get(\"/crawl/status\")\nasync def crawl_status():\n \"\"\"Check if Crawl4AI is available.\"\"\"\n try:\n import crawl4ai\n return {\"available\": True, \"version\": getattr(crawl4ai, \"__version__\", \"unknown\")}\n except ImportError:\n return {\"available\": False, \"fallback\": \"shadowglass-page-content\"}\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\nif __name__ == \"__main__\":\n import uvicorn\n\n logger.remove()\n logger.add(\n sys.stderr,\n format=\"<green>{time:HH:mm:ss}</green> | <level>{level:<7}</level> | <cyan>{name}</cyan> | {message}\",\n level=\"INFO\",\n )\n logger.add(\n Path(os.environ.get(\"APPDATA\", \".\")) / \"ShadowGlass\" / \"browser-use-bridge.log\",\n rotation=\"10 MB\",\n retention=\"7 days\",\n level=\"DEBUG\",\n )\n\n uvicorn.run(\n app,\n host=BRIDGE_HOST,\n port=BRIDGE_PORT,\n log_level=\"info\",\n )\n"}}}}