refactor(tools): compact bot-mode DM/relay/probe and browser supervisor modules

This commit is contained in:
Teknium
2026-09-02 22:26:43 -07:00
parent 113f04616b
commit 88d1423e99
9 changed files with 629 additions and 1391 deletions
+75 -137
View File
@@ -1,17 +1,15 @@
"""Dialog capture + response half of the CDP supervisor.
Two capture paths feed the same ``PendingDialog`` queue:
Two capture paths feed the same ``PendingDialog`` queue: native
``Page.javascriptDialogOpening`` events (answered with
``Page.handleJavaScriptDialog``), and the injected *dialog bridge* — a page
script that rewrites alert/confirm/prompt into a sync XHR to a magic host we
intercept via the CDP ``Fetch`` domain and answer with ``Fetch.fulfillRequest``.
The bridge works on Browserbase, whose CDP proxy auto-dismisses real native
dialogs, because the native dialog never fires.
* native ``Page.javascriptDialogOpening`` events (answered with
``Page.handleJavaScriptDialog``), and
* the injected *dialog bridge*: a page script that rewrites alert/confirm/prompt
into a sync XHR to a magic host we intercept via the CDP ``Fetch`` domain and
answer with ``Fetch.fulfillRequest``. Works on Browserbase, whose CDP proxy
auto-dismisses real native dialogs, because the native dialog never fires.
``DialogSupervisionMixin`` is mixed into ``tools.browser_supervisor.CDPSupervisor``
and relies on the state that class initialises (``_state_lock``,
``_pending_dialogs``, ``_recent_dialogs``, ``_dialog_watchdogs``, ``_cdp`` ...).
``DialogSupervisionMixin`` relies on state ``CDPSupervisor.__init__`` sets
(``_state_lock``, ``_pending_dialogs``, ``_recent_dialogs``, ``_dialog_watchdogs``, ``_cdp`` ...).
"""
from __future__ import annotations
@@ -41,7 +39,13 @@ def _trim_ring(events: list, keep: int) -> list:
return events[-keep:] if len(events) > keep * 2 else events
# ── Policy / config defaults ─────────────────────────────────────────────────
_REDACTED_FIELDS = frozenset({"message", "default_prompt"})
def _dialog_dict(obj: Any, keys: tuple) -> Dict[str, Any]:
"""Snapshot dict of ``keys`` with page-originated text fields redacted."""
return {k: _redact_supervisor_text(getattr(obj, k)) if k in _REDACTED_FIELDS else getattr(obj, k) for k in keys}
DIALOG_POLICY_MUST_RESPOND = "must_respond"
DIALOG_POLICY_AUTO_DISMISS = "auto_dismiss"
@@ -110,9 +114,6 @@ _DIALOG_BRIDGE_SCRIPT = r"""
"""
# ── Data model ────────────────────────────────────────────────────────────────
@dataclass
class PendingDialog:
"""A JS dialog currently open on some frame's session."""
@@ -129,14 +130,7 @@ class PendingDialog:
bridge_request_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"type": self.type,
"message": _redact_supervisor_text(self.message),
"default_prompt": _redact_supervisor_text(self.default_prompt),
"opened_at": self.opened_at,
"frame_id": self.frame_id,
}
return _dialog_dict(self, ("id", "type", "message", "default_prompt", "opened_at", "frame_id"))
@dataclass
@@ -152,18 +146,7 @@ class DialogRecord:
frame_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"type": self.type,
"message": _redact_supervisor_text(self.message),
"opened_at": self.opened_at,
"closed_at": self.closed_at,
"closed_by": self.closed_by,
"frame_id": self.frame_id,
}
# ── Mixin ─────────────────────────────────────────────────────────────────────
return _dialog_dict(self, ("id", "type", "message", "opened_at", "closed_at", "closed_by", "frame_id"))
class DialogSupervisionMixin:
@@ -180,41 +163,30 @@ class DialogSupervisionMixin:
async def _install_dialog_bridge(self, session_id: str) -> None:
"""Install the dialog-bridge init script + Fetch interceptor on a session.
The JS override runs in every frame before page scripts; Fetch.enable
scoped to the bridge URL catches the XHRs, which surface as pending
dialogs and are fulfilled when the agent responds. Idempotent at the CDP
level (Chromium de-dupes identical add-script calls; Fetch.enable
replaces prior patterns). The final Runtime.evaluate injects into the
already-loaded document so existing pages pick up the override on reconnect.
Idempotent at the CDP level (Chromium de-dupes identical add-script
calls; Fetch.enable replaces prior patterns). The final Runtime.evaluate
injects into the already-loaded document so existing pages pick up the
override on reconnect.
"""
sid = (session_id or "")[:16]
await self._cdp_quiet(
"Page.addScriptToEvaluateOnNewDocument",
{"source": _DIALOG_BRIDGE_SCRIPT, "runImmediately": True},
session_id=session_id, timeout=5.0, what=f"dialog bridge sid={sid}",
)
await self._cdp_quiet(
"Fetch.enable",
{"patterns": [{"urlPattern": DIALOG_BRIDGE_URL_PATTERN, "requestStage": "Request"}],
"handleAuthRequests": False},
session_id=session_id, timeout=5.0, what=f"dialog bridge sid={sid}",
)
await self._cdp_quiet(
"Runtime.evaluate",
{"expression": _DIALOG_BRIDGE_SCRIPT, "returnByValue": True},
session_id=session_id, timeout=3.0, what=f"dialog bridge inject sid={sid}",
steps = (
("Page.addScriptToEvaluateOnNewDocument", {"source": _DIALOG_BRIDGE_SCRIPT, "runImmediately": True},
5.0, f"dialog bridge sid={sid}"),
("Fetch.enable", {"patterns": [{"urlPattern": DIALOG_BRIDGE_URL_PATTERN, "requestStage": "Request"}],
"handleAuthRequests": False}, 5.0, f"dialog bridge sid={sid}"),
("Runtime.evaluate", {"expression": _DIALOG_BRIDGE_SCRIPT, "returnByValue": True},
3.0, f"dialog bridge inject sid={sid}"),
)
for method, params, timeout, what in steps:
await self._cdp_quiet(method, params, session_id=session_id, timeout=timeout, what=what)
# ── Capture ──────────────────────────────────────────────────────────────
async def _on_dialog_opening(self, params: Dict[str, Any], session_id: Optional[str]) -> None:
self._admit_dialog(self._new_dialog(
type=str(params.get("type") or ""),
message=str(params.get("message") or ""),
default_prompt=str(params.get("defaultPrompt") or ""),
session_id=session_id,
frame_id=params.get("frameId"),
))
self._admit_dialog(
type=str(params.get("type") or ""), message=str(params.get("message") or ""),
default_prompt=str(params.get("defaultPrompt") or ""), session_id=session_id, frame_id=params.get("frameId"),
)
async def _on_fetch_paused(self, params: Dict[str, Any], session_id: Optional[str]) -> None:
"""Bridge XHR captured mid-flight — materialize as a pending dialog.
@@ -231,40 +203,28 @@ class DialogSupervisionMixin:
await self._cdp_quiet("Fetch.continueRequest", {"requestId": request_id},
session_id=session_id, timeout=3.0, what="passthrough")
return
q = parse_qs(urlparse(url).query)
self._admit_dialog(self._new_dialog(
type=q.get("kind", [""])[0] or "alert",
message=q.get("message", [""])[0],
default_prompt=q.get("default_prompt", [""])[0],
session_id=session_id,
frame_id=params.get("frameId"),
bridge_request_id=str(request_id),
))
def _new_dialog(self, *, type: str, message: str, default_prompt: str, session_id: Optional[str],
frame_id: Optional[str], bridge_request_id: Optional[str] = None) -> PendingDialog:
self._dialog_seq += 1
return PendingDialog(
id=f"d-{self._dialog_seq}",
type=type,
message=message,
default_prompt=default_prompt,
opened_at=time.time(),
cdp_session_id=session_id or self._page_session_id or "",
frame_id=frame_id,
bridge_request_id=bridge_request_id,
q = {k: v[0] for k, v in parse_qs(urlparse(url).query).items()}
self._admit_dialog(
type=q.get("kind") or "alert", message=q.get("message", ""), default_prompt=q.get("default_prompt", ""),
session_id=session_id, frame_id=params.get("frameId"), bridge_request_id=str(request_id),
)
def _admit_dialog(self, dialog: PendingDialog) -> None:
"""Apply the dialog policy: auto-respond, or queue + arm the watchdog.
def _admit_dialog(self, *, type: str, message: str, default_prompt: str, session_id: Optional[str],
frame_id: Optional[str], bridge_request_id: Optional[str] = None) -> None:
"""Create the dialog and apply the policy: auto-respond, or queue + arm the watchdog.
Auto policies archive FIRST (tagged ``auto_policy``) so the ``closed``
event that follows our own response isn't re-archived as ``remote``.
"""
auto = {
DIALOG_POLICY_AUTO_DISMISS: (False, ""),
DIALOG_POLICY_AUTO_ACCEPT: (True, dialog.default_prompt),
}.get(self.dialog_policy)
self._dialog_seq += 1
dialog = PendingDialog(
id=f"d-{self._dialog_seq}", type=type, message=message, default_prompt=default_prompt,
opened_at=time.time(), cdp_session_id=session_id or self._page_session_id or "",
frame_id=frame_id, bridge_request_id=bridge_request_id,
)
auto = {DIALOG_POLICY_AUTO_DISMISS: (False, ""), DIALOG_POLICY_AUTO_ACCEPT: (True, default_prompt)}.get(
self.dialog_policy
)
if auto is not None:
with self._state_lock:
self._archive_dialog_locked(dialog, "auto_policy")
@@ -280,37 +240,26 @@ class DialogSupervisionMixin:
# ── Responding ───────────────────────────────────────────────────────────
async def _respond(self, dialog: PendingDialog, *, accept: bool, prompt_text: Optional[str]) -> None:
"""Bridge-fulfill for XHR-captured dialogs, else native CDP.
Native path sends ``promptText`` only for prompt dialogs when
``prompt_text`` is given, and raises on CDP failure; the bridge path
(Fetch.fulfillRequest so the page unblocks) swallows failures.
"""
"""Bridge-fulfill for XHR-captured dialogs (swallows failures so the page
unblocks), else native CDP — ``promptText`` only for prompt dialogs when
given; raises on CDP failure."""
session_id = dialog.cdp_session_id or None
if dialog.bridge_request_id:
body = json.dumps({
"accept": bool(accept),
"prompt_text": (prompt_text or "") if dialog.type == "prompt" else "",
"dialog_id": dialog.id,
}).encode()
body = json.dumps({"accept": bool(accept), "dialog_id": dialog.id,
"prompt_text": (prompt_text or "") if dialog.type == "prompt" else ""}).encode()
await self._cdp_quiet(
"Fetch.fulfillRequest",
{
"requestId": dialog.bridge_request_id,
"responseCode": 200,
"responseHeaders": [
{"name": "Content-Type", "value": "application/json"},
{"name": "Access-Control-Allow-Origin", "value": "*"},
],
"body": base64.b64encode(body).decode(),
},
session_id=dialog.cdp_session_id or None, timeout=5.0, what=f"bridge fulfill {dialog.id}",
{"requestId": dialog.bridge_request_id, "responseCode": 200,
"responseHeaders": [{"name": "Content-Type", "value": "application/json"},
{"name": "Access-Control-Allow-Origin", "value": "*"}],
"body": base64.b64encode(body).decode()},
session_id=session_id, timeout=5.0, what=f"bridge fulfill {dialog.id}",
)
return
params: Dict[str, Any] = {"accept": accept}
if prompt_text is not None and dialog.type == "prompt":
params["promptText"] = prompt_text
await self._cdp("Page.handleJavaScriptDialog", params,
session_id=dialog.cdp_session_id or None, timeout=5.0)
await self._cdp("Page.handleJavaScriptDialog", params, session_id=session_id, timeout=5.0)
async def _respond_quiet(self, dialog: PendingDialog, *, accept: bool, prompt_text: Optional[str]) -> None:
"""Auto-policy / watchdog response (already archived by the caller); failures logged only."""
@@ -320,11 +269,8 @@ class DialogSupervisionMixin:
logger.debug("auto response failed for %s: %s", dialog.id, e)
async def _handle_dialog_cdp(self, dialog: PendingDialog, *, accept: bool, prompt_text: str) -> None:
"""Agent response path.
The dialog is retired regardless of outcome — a CDP error usually means
it already closed (browser auto-dismissed after navigation, etc.).
"""
"""Agent response path. The dialog is retired regardless of outcome — a CDP
error usually means it already closed (browser auto-dismissed after navigation)."""
try:
await self._respond(dialog, accept=accept, prompt_text=prompt_text)
finally:
@@ -335,14 +281,10 @@ class DialogSupervisionMixin:
dialog = self._pending_dialogs.get(dialog_id)
if dialog is None:
return
logger.warning(
"CDP supervisor %s: dialog %s (%s) auto-dismissed after %ss timeout",
self.task_id, dialog_id, dialog.type, self.dialog_timeout_s,
)
logger.warning("CDP supervisor %s: dialog %s (%s) auto-dismissed after %ss timeout",
self.task_id, dialog_id, dialog.type, self.dialog_timeout_s)
# Archive with watchdog tag BEFORE unblocking the page.
with self._state_lock:
if self._pending_dialogs.pop(dialog_id, None) is not None:
self._archive_dialog_locked(dialog, "watchdog")
self._retire_dialog(dialog_id, "watchdog")
await self._respond_quiet(dialog, accept=False, prompt_text=None)
# ── Bookkeeping ──────────────────────────────────────────────────────────
@@ -359,11 +301,9 @@ class DialogSupervisionMixin:
def _archive_dialog_locked(self, dialog: PendingDialog, closed_by: str) -> None:
"""Move a pending dialog to the recent_dialogs ring buffer. Must hold state_lock."""
self._recent_dialogs.append(DialogRecord(
id=dialog.id, type=dialog.type, message=dialog.message, opened_at=dialog.opened_at,
closed_at=time.time(), closed_by=closed_by, frame_id=dialog.frame_id,
))
self._recent_dialogs = _trim_ring(self._recent_dialogs, RECENT_DIALOGS_MAX)
record = DialogRecord(id=dialog.id, type=dialog.type, message=dialog.message, opened_at=dialog.opened_at,
closed_at=time.time(), closed_by=closed_by, frame_id=dialog.frame_id)
self._recent_dialogs = _trim_ring([*self._recent_dialogs, record], RECENT_DIALOGS_MAX)
async def _on_dialog_closed(self, params: Dict[str, Any], session_id: Optional[str]) -> None:
# ``Page.javascriptDialogClosed`` carries only ``result``/``userInput``, not
@@ -371,12 +311,10 @@ class DialogSupervisionMixin:
# it — the JS thread blocks while a dialog is up, so at most one is in
# flight per session. Bridge dialogs resolve via Fetch.fulfillRequest.
with self._state_lock:
candidate_ids = [
d.id for d in self._pending_dialogs.values()
if d.cdp_session_id == session_id and d.bridge_request_id is None
]
if candidate_ids:
self._retire_dialog(candidate_ids[0], "remote")
candidate = next((d.id for d in self._pending_dialogs.values()
if d.cdp_session_id == session_id and d.bridge_request_id is None), None)
if candidate:
self._retire_dialog(candidate, "remote")
# CDP event → handler(self, params, session_id); merged into CDPSupervisor._EVENT_HANDLERS.
EVENT_HANDLERS: Dict[str, Callable[..., Any]] = {