unbroker

Autonomously remove your info from data-broker sites.

  • privacy
  • data-broker
  • opt-out
  • ccpa
  • gdpr
  • security
  • doxxing

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/optional-skills/security/unbroker'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

scripts/cdp.py

scripts/cdp.pyBrowse 56 files
View on GitHub
← Back to SKILL.md
#!/usr/bin/env python3"""Launch (or detect) the operator's local Chrome/Chromium over the DevTools Protocol (CDP). Phase-2 work -- sending opt-out/CCPA email through the operator's logged-in webmail, and drivingsession-bound multi-step opt-out gates (e.g. PeopleConnect guided-mode) -- must run in theoperator's OWN browser: real fingerprint, residential IP, and the operator's signed-in sessions.A headless cloud browser (Browserbase) is the wrong tool there (it has no webmail session and isitself anti-bot-gated on those exact flows). This module launches the operator's real Chrome withremote debugging on a DEDICATED profile so Hermes's browser tools can attach at 127.0.0.1:<port>. Stdlib only; cross-platform (macOS / Linux / Windows). Nothing here touches a password or PII."""from __future__ import annotations import jsonimport osimport shutilimport subprocessimport sysimport urllib.errorimport urllib.requestfrom pathlib import Path import paths DEFAULT_PORT = 9222 # Chromium-family binaries we know how to drive, in preference order. Names first (works on any OS# where one is on PATH), then per-OS absolute-path fallbacks below._PATH_NAMES = (    "google-chrome", "google-chrome-stable", "chromium", "chromium-browser",    "brave-browser", "microsoft-edge", "microsoft-edge-stable", "chrome",)  def default_profile() -> Path:    """Dedicated debug profile dir, NOT the operator's Default Chrome profile.     Chrome refuses remote-debugging on a profile that is already open in another Chrome instance,    so we isolate the debug session in its own user-data-dir under HERMES_HOME.    """    return paths.hermes_home() / "chrome-debug"  def _mac_candidates() -> list[str]:    return [        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",        "/Applications/Chromium.app/Contents/MacOS/Chromium",        "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",        "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",        "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",    ]  def _windows_candidates() -> list[str]:    bases = [        os.environ.get("ProgramFiles", r"C:\Program Files"),        os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),        os.environ.get("LOCALAPPDATA", ""),    ]    rels = [        r"Google\Chrome\Application\chrome.exe",        r"Chromium\Application\chrome.exe",        r"BraveSoftware\Brave-Browser\Application\brave.exe",        r"Microsoft\Edge\Application\msedge.exe",    ]    out: list[str] = []    for base in bases:        if not base:            continue        for rel in rels:            out.append(str(Path(base) / rel))    return out  def find_browser(override: str | None = None) -> str | None:    """Return the first usable Chromium-family browser path/command, or None.     `override` (an explicit path, or a command on PATH) wins when it resolves.    """    if override:        if Path(override).exists():            return override        return shutil.which(override)  # may be None -> caller reports "not found"    for name in _PATH_NAMES:        found = shutil.which(name)        if found:            return found    if sys.platform == "darwin":        candidates = _mac_candidates()    elif sys.platform == "win32":        candidates = _windows_candidates()    else:        candidates = []    for cand in candidates:        if Path(cand).exists():            return cand    return None  def launch_command(browser: str, port: int = DEFAULT_PORT, profile: Path | None = None) -> list[str]:    """The exact argv used to start the debug browser (also handy for `--print`)."""    profile = profile or default_profile()    return [        browser,        f"--remote-debugging-port={int(port)}",        f"--user-data-dir={profile}",        "--no-first-run",        "--no-default-browser-check",    ]  def _http_get(url: str, timeout: float) -> bytes:    req = urllib.request.Request(url, headers={"User-Agent": "unbroker-cdp/1.0"})    with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310 (localhost only)        return resp.read()  def endpoint_status(port: int = DEFAULT_PORT, host: str = "127.0.0.1",                    timeout: float = 1.0) -> dict | None:    """Return the CDP `/json/version` dict if a debuggable browser is live at host:port, else None.     (Chrome restricts this endpoint to localhost/IP Host headers, so we always hit 127.0.0.1.)    """    url = f"http://{host}:{int(port)}/json/version"    try:        raw = _http_get(url, timeout)    except (urllib.error.URLError, TimeoutError, ConnectionError, OSError, ValueError):        return None    try:        data = json.loads(raw.decode("utf-8", errors="replace"))    except (ValueError, AttributeError):        return None    return data if isinstance(data, dict) else None  def launch(browser: str, port: int = DEFAULT_PORT, profile: Path | None = None) -> int:    """Start the browser detached with remote debugging; return the child PID.     Detach so the browser outlives this short-lived CLI call. POSIX uses start_new_session (which    avoids referencing os.setsid, so there is no Windows import-time footgun); Windows uses    DETACHED_PROCESS + a new process group.    """    profile = profile or default_profile()    profile.mkdir(parents=True, exist_ok=True)    cmd = launch_command(browser, port, profile)    kwargs: dict = {        "stdin": subprocess.DEVNULL,        "stdout": subprocess.DEVNULL,        "stderr": subprocess.DEVNULL,    }    if sys.platform == "win32":        kwargs["creationflags"] = (            subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP  # windows-footgun: ok        )    else:        kwargs["start_new_session"] = True    proc = subprocess.Popen(cmd, **kwargs)    return proc.pid