scripts/cdp.py
scripts/cdp.pyBrowse 56 files
1,412 tokens
5,914 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Launch (or detect) the operator's local Chrome/Chromium over the DevTools Protocol (CDP).3 4Phase-2 work -- sending opt-out/CCPA email through the operator's logged-in webmail, and driving5session-bound multi-step opt-out gates (e.g. PeopleConnect guided-mode) -- must run in the6operator's OWN browser: real fingerprint, residential IP, and the operator's signed-in sessions.7A headless cloud browser (Browserbase) is the wrong tool there (it has no webmail session and is8itself anti-bot-gated on those exact flows). This module launches the operator's real Chrome with9remote debugging on a DEDICATED profile so Hermes's browser tools can attach at 127.0.0.1:<port>.10 11Stdlib only; cross-platform (macOS / Linux / Windows). Nothing here touches a password or PII.12"""13from __future__ import annotations14 15import json16import os17import shutil18import subprocess19import sys20import urllib.error21import urllib.request22from pathlib import Path23 24import paths25 26DEFAULT_PORT = 922227 28# Chromium-family binaries we know how to drive, in preference order. Names first (works on any OS29# where one is on PATH), then per-OS absolute-path fallbacks below.30_PATH_NAMES = (31 "google-chrome", "google-chrome-stable", "chromium", "chromium-browser",32 "brave-browser", "microsoft-edge", "microsoft-edge-stable", "chrome",33)34 35 36def default_profile() -> Path:37 """Dedicated debug profile dir, NOT the operator's Default Chrome profile.38 39 Chrome refuses remote-debugging on a profile that is already open in another Chrome instance,40 so we isolate the debug session in its own user-data-dir under HERMES_HOME.41 """42 return paths.hermes_home() / "chrome-debug"43 44 45def _mac_candidates() -> list[str]:46 return [47 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",48 "/Applications/Chromium.app/Contents/MacOS/Chromium",49 "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",50 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",51 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",52 ]53 54 55def _windows_candidates() -> list[str]:56 bases = [57 os.environ.get("ProgramFiles", r"C:\Program Files"),58 os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),59 os.environ.get("LOCALAPPDATA", ""),60 ]61 rels = [62 r"Google\Chrome\Application\chrome.exe",63 r"Chromium\Application\chrome.exe",64 r"BraveSoftware\Brave-Browser\Application\brave.exe",65 r"Microsoft\Edge\Application\msedge.exe",66 ]67 out: list[str] = []68 for base in bases:69 if not base:70 continue71 for rel in rels:72 out.append(str(Path(base) / rel))73 return out74 75 76def find_browser(override: str | None = None) -> str | None:77 """Return the first usable Chromium-family browser path/command, or None.78 79 `override` (an explicit path, or a command on PATH) wins when it resolves.80 """81 if override:82 if Path(override).exists():83 return override84 return shutil.which(override) # may be None -> caller reports "not found"85 for name in _PATH_NAMES:86 found = shutil.which(name)87 if found:88 return found89 if sys.platform == "darwin":90 candidates = _mac_candidates()91 elif sys.platform == "win32":92 candidates = _windows_candidates()93 else:94 candidates = []95 for cand in candidates:96 if Path(cand).exists():97 return cand98 return None99 100 101def launch_command(browser: str, port: int = DEFAULT_PORT, profile: Path | None = None) -> list[str]:102 """The exact argv used to start the debug browser (also handy for `--print`)."""103 profile = profile or default_profile()104 return [105 browser,106 f"--remote-debugging-port={int(port)}",107 f"--user-data-dir={profile}",108 "--no-first-run",109 "--no-default-browser-check",110 ]111 112 113def _http_get(url: str, timeout: float) -> bytes:114 req = urllib.request.Request(url, headers={"User-Agent": "unbroker-cdp/1.0"})115 with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (localhost only)116 return resp.read()117 118 119def endpoint_status(port: int = DEFAULT_PORT, host: str = "127.0.0.1",120 timeout: float = 1.0) -> dict | None:121 """Return the CDP `/json/version` dict if a debuggable browser is live at host:port, else None.122 123 (Chrome restricts this endpoint to localhost/IP Host headers, so we always hit 127.0.0.1.)124 """125 url = f"http://{host}:{int(port)}/json/version"126 try:127 raw = _http_get(url, timeout)128 except (urllib.error.URLError, TimeoutError, ConnectionError, OSError, ValueError):129 return None130 try:131 data = json.loads(raw.decode("utf-8", errors="replace"))132 except (ValueError, AttributeError):133 return None134 return data if isinstance(data, dict) else None135 136 137def launch(browser: str, port: int = DEFAULT_PORT, profile: Path | None = None) -> int:138 """Start the browser detached with remote debugging; return the child PID.139 140 Detach so the browser outlives this short-lived CLI call. POSIX uses start_new_session (which141 avoids referencing os.setsid, so there is no Windows import-time footgun); Windows uses142 DETACHED_PROCESS + a new process group.143 """144 profile = profile or default_profile()145 profile.mkdir(parents=True, exist_ok=True)146 cmd = launch_command(browser, port, profile)147 kwargs: dict = {148 "stdin": subprocess.DEVNULL,149 "stdout": subprocess.DEVNULL,150 "stderr": subprocess.DEVNULL,151 }152 if sys.platform == "win32":153 kwargs["creationflags"] = (154 subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP # windows-footgun: ok155 )156 else:157 kwargs["start_new_session"] = True158 proc = subprocess.Popen(cmd, **kwargs)159 return proc.pid160