#!/usr/bin/env python3 """browser-broker — one owner for the logged-in Brave, so agents stop fighting. Every agent on this machine drives the SAME Brave through CDP on 9222, and CDP has no concept of ownership: two agents that both go looking for a tab land on the same one, and one navigating it out from under the other is invisible to both. Worse, an agent that grabs "the active tab" grabs whatever the human just clicked on. This process owns 9222. Agents ask it for a LEASE and get an exclusive claim on a target they alone may touch. The ownership rule is deliberately dumb, because clever focus-detection breaks across macOS Spaces: **the broker can only hand out tabs the broker opened.** Anything the human opened is invisible to every agent, permanently, with no heuristics involved. There is an /adopt escape hatch for the rare "drive this existing tab" case, and it requires saying so explicitly. Leases expire, so a crashed agent releases its tab instead of deadlocking the browser forever. Work tabs are ordinary BACKGROUND tabs in the window the human already has open — never a separate window, never headless. Launch the browser with --disable-backgrounding-occluded-windows and --disable-renderer-backgrounding (see launch-browser.sh): Chromium throttles background-tab timers to about 1Hz, and a throttled tab mounts zero rows in a modern SPA. The flags keep timers and fetch callbacks running; they do NOT make a hidden tab paint. requestAnimationFrame never fires while visibilityState is "hidden", so a rAF-driven page (canvas, animated charts) stays on its first frame no matter what flags you pass. Listens on 127.0.0.1:9223. Never exposed off the box. """ import json, threading, time, uuid, sys, os from datetime import datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.request import urlopen from urllib.error import URLError from websocket import create_connection, WebSocketException # Everything is overridable by environment so the same file runs anywhere. CDP_PORT = int(os.environ.get("BROKER_CDP_PORT", 9222)) LISTEN = ("127.0.0.1", int(os.environ.get("BROKER_PORT", 9223))) DEFAULT_TTL = int(os.environ.get("BROKER_TTL", 300)) # seconds an unrenewed lease survives REAP_EVERY = 5 # seconds between expiry sweeps HEARTBEAT_EVERY = int(os.environ.get("BROKER_HEARTBEAT", 3600)) # one proof-of-life line per hour LOG = os.path.expanduser(os.environ.get("BROKER_LOG", os.path.join(os.path.dirname(os.path.abspath(__file__)), "broker.log"))) def log(msg): line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}" print(line, flush=True) try: with open(LOG, "a") as f: f.write(line + "\n") except OSError: pass class CDPError(Exception): pass class Browser: """Browser-level CDP connection, reconnecting on its own. Brave dies under us regularly — it auto-updates and gets restarted by brave-update-fixer. Every call goes through _send, which reconnects once before giving up, so a restart costs a retry rather than a dead broker. """ def __init__(self): self._ws = None self._id = 0 self._lock = threading.Lock() def _ws_url(self): with urlopen(f"http://127.0.0.1:{CDP_PORT}/json/version", timeout=5) as r: return json.load(r)["webSocketDebuggerUrl"] def _connect(self): self._ws = create_connection(self._ws_url(), suppress_origin=True, timeout=20) def _send_once(self, method, **params): if self._ws is None: self._connect() self._id += 1 mid = self._id self._ws.send(json.dumps({"id": mid, "method": method, "params": params})) while True: msg = json.loads(self._ws.recv()) if msg.get("id") == mid: if "error" in msg: raise CDPError(msg["error"].get("message", str(msg["error"]))) return msg.get("result", {}) def send(self, method, **params): with self._lock: try: return self._send_once(method, **params) except (WebSocketException, OSError, URLError, json.JSONDecodeError): try: if self._ws: self._ws.close() except Exception: pass self._ws = None return self._send_once(method, **params) def alive(self): try: self.send("Browser.getVersion") return True except Exception: return False def targets(self): return {t["targetId"] for t in self.send("Target.getTargets").get("targetInfos", []) if t.get("type") == "page"} class Registry: """Which targets the broker opened, and who currently holds each one.""" def __init__(self, browser): self.b = browser self.lock = threading.RLock() self.owned = {} # target_id -> {"created": ts, "hidden": bool} self.leases = {} # lease_id -> {target_id, owner, purpose, expires} # ---- leases -------------------------------------------------------- def open_lease(self, owner, url, purpose, ttl, hidden=True): with self.lock: # An ordinary background tab in the window that is already open. # This used to be a new window parked at x=-4200. It rendered fine # and never took focus, but the window still EXISTED, so every agent # run shoved the human's window aside -- twice over, with a broker # on two machines. Isolation was never the window's job: the # ownership registry plus the swallowed focus commands do that. # background=True opens it behind whatever the human is looking at. res = self.b.send("Target.createTarget", url=url or "about:blank", background=True) tid = res["targetId"] self.owned[tid] = {"created": time.time(), "hidden": hidden} return self._grant(tid, owner, purpose, ttl) def adopt_lease(self, owner, target_id, purpose, ttl): """Explicitly take over a tab the broker did not open. Deliberate only.""" with self.lock: if target_id not in self.b.targets(): raise KeyError("no such target") if self._holder(target_id): raise PermissionError("target already leased") self.owned[target_id] = {"created": time.time(), "hidden": False, "adopted": True} return self._grant(target_id, owner, purpose, ttl) def _grant(self, tid, owner, purpose, ttl): lid = uuid.uuid4().hex[:12] self.leases[lid] = {"target_id": tid, "owner": owner, "purpose": purpose, "expires": time.time() + ttl} log(f"lease {lid} -> {owner} on {tid[:12]} ({purpose or 'no purpose given'})") return {"lease_id": lid, "target_id": tid, "ws_url": f"ws://127.0.0.1:{CDP_PORT}/devtools/page/{tid}", "expires_in": ttl} def _holder(self, tid): for lid, l in self.leases.items(): if l["target_id"] == tid and l["expires"] > time.time(): return lid return None def renew(self, lease_id, ttl): with self.lock: l = self.leases.get(lease_id) if not l: raise KeyError("unknown lease") l["expires"] = time.time() + ttl return {"lease_id": lease_id, "expires_in": ttl} def release(self, lease_id, close=True): with self.lock: l = self.leases.pop(lease_id, None) if not l: raise KeyError("unknown lease") tid = l["target_id"] if close and tid in self.owned and not self.owned[tid].get("adopted"): try: self.b.send("Target.closeTarget", targetId=tid) except CDPError: pass self.owned.pop(tid, None) log(f"lease {lease_id} released by {l['owner']}") return {"released": lease_id} def reap(self): """Expire stale leases. A crashed agent must not hold a tab forever.""" with self.lock: now = time.time() dead = [lid for lid, l in self.leases.items() if l["expires"] <= now] for lid in dead: l = self.leases[lid] log(f"lease {lid} EXPIRED (owner {l['owner']}) — reclaiming tab") try: self.release(lid, close=True) except KeyError: pass # Drop bookkeeping for tabs that vanished (Brave restart, manual close). if dead or self.owned: try: live = self.b.targets() except Exception: return for tid in list(self.owned): if tid not in live: self.owned.pop(tid, None) def status(self): with self.lock: now = time.time() return { "leases": [{"lease_id": lid, "owner": l["owner"], "purpose": l["purpose"], "target": l["target_id"][:12], "expires_in": round(l["expires"] - now, 1)} for lid, l in self.leases.items()], "broker_tabs": len(self.owned), "brave_alive": self.b.alive(), } class Handler(BaseHTTPRequestHandler): registry = None def log_message(self, *a): pass # our own log() only def _reply(self, code, obj): body = json.dumps(obj).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _body(self): n = int(self.headers.get("Content-Length") or 0) return json.loads(self.rfile.read(n) or b"{}") def do_GET(self): if self.path.startswith("/status"): return self._reply(200, self.registry.status()) if self.path.startswith("/health"): return self._reply(200, {"ok": True, "brave": self.registry.b.alive()}) self._reply(404, {"error": "no such endpoint"}) def do_POST(self): try: b = self._body() r = self.registry if self.path.startswith("/lease"): return self._reply(200, r.open_lease( b.get("owner", "anonymous"), b.get("url"), b.get("purpose"), int(b.get("ttl", DEFAULT_TTL)), bool(b.get("hidden", True)))) if self.path.startswith("/adopt"): return self._reply(200, r.adopt_lease( b.get("owner", "anonymous"), b["target_id"], b.get("purpose"), int(b.get("ttl", DEFAULT_TTL)))) if self.path.startswith("/renew"): return self._reply(200, r.renew(b["lease_id"], int(b.get("ttl", DEFAULT_TTL)))) if self.path.startswith("/release"): return self._reply(200, r.release(b["lease_id"], bool(b.get("close", True)))) self._reply(404, {"error": "no such endpoint"}) except KeyError as e: self._reply(404, {"error": str(e)}) except PermissionError as e: self._reply(409, {"error": str(e)}) except Exception as e: self._reply(500, {"error": f"{type(e).__name__}: {e}"}) def background(registry): """Reaper + heartbeat. The heartbeat is not decoration: brave-update-fixer sat loaded and dead for six weeks because a silent exit 0 looks exactly like a healthy one. Silence in this log means the broker itself died.""" last_hb = 0 while True: try: registry.reap() if time.time() - last_hb > HEARTBEAT_EVERY: s = registry.status() log(f"heartbeat — {len(s['leases'])} leases, {s['broker_tabs']} broker tabs, " f"brave {'up' if s['brave_alive'] else 'DOWN'}") last_hb = time.time() except Exception as e: log(f"background error: {type(e).__name__}: {e}") time.sleep(REAP_EVERY) def main(): b = Browser() if not b.alive(): log(f"no browser answering CDP on {CDP_PORT} — launch Chrome/Brave with " f"--remote-debugging-port={CDP_PORT} first (see launch-browser.sh)") sys.exit(1) reg = Registry(b) Handler.registry = reg threading.Thread(target=background, args=(reg,), daemon=True).start() srv = ThreadingHTTPServer(LISTEN, Handler) log(f"broker up on http://{LISTEN[0]}:{LISTEN[1]} — owning CDP {CDP_PORT}") srv.serve_forever() if __name__ == "__main__": main()