corona→corona: academic Corona now only in lp-220-p3q-corona

This commit is contained in:
2026-06-11 10:26:04 -07:00
parent 0335911d28
commit 586effb3bf
2 changed files with 294 additions and 37 deletions
@@ -163,20 +163,116 @@ def _zap_method_for(action: str) -> str:
the JSON it receives. Over ZAP we send the same method name so the
extension handler is shared.
"""
# Decomplected API surface — three layers, orthogonal:
# 1) CDP-shape canonical names (Domain.method)
# 2) hanzo.* ergonomic aliases that compose CDP primitives
# 3) Python-side snake_case shortcuts that map to either
return {
# ─── Page lifecycle / navigation ────────────────────────────────
"navigate": "Page.navigate",
"screenshot": "hanzo.screenshot",
"click": "hanzo.click",
"fill": "hanzo.fill",
"evaluate": "Runtime.evaluate",
"tabs": "Target.getTargets",
"status": "Browser.getVersion",
"url": "hanzo.url",
"title": "hanzo.title",
"reload": "Page.reload",
"go_back": "Page.goBack",
"go_forward": "Page.goForward",
"print_pdf": "Page.printToPDF",
"wait_for_navigation": "hanzo.waitForNavigation",
"wait_for_load_state": "Page.waitForLoadState",
# ─── Tabs / targets ─────────────────────────────────────────────
"tabs": "Target.getTargets",
"new_tab": "Target.createTarget",
"close_tab": "Target.closeTarget",
"activate_tab": "Target.activateTarget",
"url": "hanzo.url",
"title": "hanzo.title",
"tab_info": "hanzo.tabInfo",
"list_tabs": "hanzo.listTabs",
"history": "hanzo.getHistory",
# ─── Observation ────────────────────────────────────────────────
"screenshot": "hanzo.screenshot",
"page_info": "hanzo.getPageInfo",
"ax_tree": "Accessibility.getFullAXTree",
"status": "Browser.getVersion",
# ─── DOM read ───────────────────────────────────────────────────
"get_text": "hanzo.getText",
"get_html": "hanzo.getHTML",
"get_attribute": "hanzo.getAttribute",
"get_element_info": "hanzo.getElementInfo",
"query_one": "DOM.querySelector",
"query_all": "hanzo.querySelectorAll",
"list_form": "hanzo.listForm",
"computed_styles": "hanzo.getComputedStyles",
"bounding_rects": "hanzo.getBoundingRects",
# ─── DOM write — selector-based ─────────────────────────────────
"click": "hanzo.click",
"dblclick": "hanzo.dblclick",
"hover": "hanzo.hover",
"fill": "hanzo.fill",
"check": "hanzo.check",
"uncheck": "hanzo.uncheck",
"select": "hanzo.select",
"type": "hanzo.type",
"clear": "hanzo.clear",
"focus": "DOM.focus",
"scroll_into_view": "DOM.scrollIntoView",
"set_text": "hanzo.setText",
"set_html": "hanzo.setHTML",
"set_attribute": "hanzo.setAttribute",
"remove_attribute": "hanzo.removeAttribute",
# ─── DOM write — CSP-safe text/label-based (RECOMMENDED) ─────────
"click_text": "hanzo.clickByText",
"fill_label": "hanzo.fillByLabel",
"find_by_text": "hanzo.findByText",
"submit_form": "hanzo.submitForm",
"upload_file": "hanzo.uploadFile",
# ─── Keyboard / Mouse ───────────────────────────────────────────
"press": "hanzo.press",
"press_key": "Input.dispatchKeyEvent",
"mouse_event": "Input.dispatchMouseEvent",
"scroll": "hanzo.scroll",
"scroll_wheel": "Input.scrollWheel",
# ─── Wait / Observe ─────────────────────────────────────────────
"wait_for_text": "hanzo.waitForText",
"wait_for_mutation": "hanzo.waitForMutation",
"wait_for_selector": "hanzo.waitForSelector",
"observe_start": "hanzo.observe",
"observe_read": "hanzo.observeRead",
"observe_stop": "hanzo.observeStop",
# ─── Dialog ─────────────────────────────────────────────────────
"dialog_accept": "hanzo.dialogAccept",
# ─── Scripting (works on non-CSP-strict pages only) ─────────────
"evaluate": "Runtime.evaluate",
"inject_script": "hanzo.injectScript",
"inject_css": "hanzo.injectCSS",
# ─── Cookies / Storage ──────────────────────────────────────────
"cookies": "hanzo.getCookies",
"local_storage_get": "hanzo.getLocalStorage",
"local_storage_set": "hanzo.setLocalStorage",
# ─── Network monitoring ─────────────────────────────────────────
"monitor_start": "monitor.start",
"monitor_stop": "monitor.stop",
"monitor_console_logs": "monitor.consoleLogs",
"monitor_console_errors": "monitor.consoleErrors",
"monitor_network_logs": "monitor.networkLogs",
"monitor_network_errors": "monitor.networkErrors",
"monitor_network_success": "monitor.networkSuccess",
# ─── Audit / Lighthouse-style ───────────────────────────────────
"audit_accessibility": "audit.accessibility",
"audit_performance": "audit.performance",
"audit_seo": "audit.seo",
# ─── HTTP fetch through the browser (uses page origin) ──────────
"fetch": "hanzo.fetch",
}.get(action, action)
@@ -187,6 +283,7 @@ def _zap_params(
url: Optional[str] = None,
selector: Optional[str] = None,
text: Optional[str] = None,
label: Optional[str] = None,
value: Optional[str] = None,
code: Optional[str] = None,
expression: Optional[str] = None,
@@ -203,6 +300,8 @@ def _zap_params(
params["value"] = value
if text is not None:
params["text"] = text
if label is not None:
params["label"] = label
expr = code or expression
if expr is not None:
params["expression"] = expr
@@ -223,6 +322,9 @@ def _zap_params(
"state",
"level",
"expression",
"scope", # for click_text — "default" or "all"
"limit", # for query_all
"outer", # for get_html
}:
params[k] = v
return params
@@ -274,6 +376,23 @@ async def _extension_command(
# unwrap so the caller sees the value directly.
result = raw
if method == "Runtime.evaluate" and isinstance(raw, dict):
# Surface an evaluation error (commonly page CSP
# blocking Function()/eval) instead of silently
# flattening it to a null value — that null was
# indistinguishable from a legitimate null result.
err = raw.get("error")
exc = raw.get("exceptionDetails")
if err or exc:
msg = err or (
exc.get("text") if isinstance(exc, dict) else str(exc)
)
return {
"success": False,
"transport": "zap",
"error": msg,
"exceptionDetails": exc,
"result": None,
}
cdp = raw.get("result", raw)
if isinstance(cdp, dict) and "value" in cdp:
result = cdp["value"]
@@ -301,9 +301,25 @@ class CDPBridgeServer:
# Update target client's last_active
target_client.last_active = time.time()
# Map HTTP action to CDP method
# Map HTTP action to CDP method.
action = data.get("action", "")
method = self._action_to_method(action)
# Raw passthrough: the `cdp` action carries the real wire method in the
# `method` field (this is what CdpTool's HTTP fallback sends). Without
# this, _action_to_method("cdp") falls through to the literal "cdp",
# which the extension rejects with "Unknown method: cdp".
if action == "cdp":
method = data.get("method") or ""
if not method:
return web.json_response(
{
"error": "cdp action requires a 'method' (e.g. 'Runtime.evaluate')",
"client_id": target_client.client_id,
},
status=400,
headers={"Access-Control-Allow-Origin": "*"},
)
else:
method = self._action_to_method(action)
params = self._build_params(data)
# Forward to extension and wait for response
@@ -329,6 +345,26 @@ class CDPBridgeServer:
# Unwrap CDP-specific result formats for consistency with Playwright
raw_result = response.get("result", {})
if method == "Runtime.evaluate" and isinstance(raw_result, dict):
# An evaluation error (e.g. page CSP blocking Function()/eval)
# must NOT be flattened to a silent `null`. The extension
# surfaces it via `error` / `exceptionDetails`; propagate it so
# the caller sees *why* evaluate failed instead of a bare null.
err = raw_result.get("error")
exc = raw_result.get("exceptionDetails")
if err or exc:
msg = err or (
exc.get("text") if isinstance(exc, dict) else str(exc)
)
return web.json_response(
{
"success": False,
"client_id": target_client.client_id,
"error": msg,
"exceptionDetails": exc,
"result": None,
},
headers={"Access-Control-Allow-Origin": "*"},
)
# CDP returns {result: {type, value}} — extract the value
cdp_result = raw_result.get("result", {})
if isinstance(cdp_result, dict) and "value" in cdp_result:
@@ -368,43 +404,145 @@ class CDPBridgeServer:
)
def _action_to_method(self, action: str) -> str:
"""Map HTTP action to CDP method."""
"""Map HTTP action to CDP/wire method.
Aligned with hanzo_tools.browser.browser_tool._zap_method_for(). Same
action namespace, same expectations. If an action is already a wire
method (contains "."), it is forwarded as-is to allow direct
Page.navigate / DOM.querySelector / etc. usage.
"""
# Passthrough for already-qualified wire methods.
if "." in action:
return action
action_map = {
# Navigation / lifecycle
"navigate": "Page.navigate",
"screenshot": "hanzo.screenshot",
"click": "hanzo.click",
"fill": "hanzo.fill",
"evaluate": "Runtime.evaluate",
"tabs": "Target.getTargets",
"status": "Browser.getVersion",
"url": "hanzo.url",
"title": "hanzo.title",
"reload": "Page.reload",
"go_back": "Page.goBack",
"go_forward": "Page.goForward",
"print_pdf": "Page.printToPDF",
"wait_for_navigation": "hanzo.waitForNavigation",
"wait_for_load_state": "Page.waitForLoadState",
# Tabs / targets
"tabs": "Target.getTargets",
"new_tab": "Target.createTarget",
"close_tab": "Target.closeTarget",
"activate_tab": "Target.activateTarget",
"url": "hanzo.url",
"title": "hanzo.title",
"tab_info": "hanzo.tabInfo",
"list_tabs": "hanzo.listTabs",
"history": "hanzo.getHistory",
# Observation
"screenshot": "hanzo.screenshot",
"page_info": "hanzo.getPageInfo",
"ax_tree": "Accessibility.getFullAXTree",
"status": "Browser.getVersion",
# DOM read
"get_text": "hanzo.getText",
"get_html": "hanzo.getHTML",
"get_attribute": "hanzo.getAttribute",
"get_element_info": "hanzo.getElementInfo",
"query_one": "DOM.querySelector",
"query_all": "hanzo.querySelectorAll",
"list_form": "hanzo.listForm",
"computed_styles": "hanzo.getComputedStyles",
"bounding_rects": "hanzo.getBoundingRects",
# DOM write — selector-based
"click": "hanzo.click",
"dblclick": "hanzo.dblclick",
"hover": "hanzo.hover",
"fill": "hanzo.fill",
"check": "hanzo.check",
"uncheck": "hanzo.uncheck",
"select": "hanzo.select",
"type": "hanzo.type",
"clear": "hanzo.clear",
"focus": "DOM.focus",
"scroll_into_view": "DOM.scrollIntoView",
"set_text": "hanzo.setText",
"set_html": "hanzo.setHTML",
"set_attribute": "hanzo.setAttribute",
"remove_attribute": "hanzo.removeAttribute",
# DOM write — CSP-safe text/label-based (RECOMMENDED for forms)
"click_text": "hanzo.clickByText",
"fill_label": "hanzo.fillByLabel",
"find_by_text": "hanzo.findByText",
"submit_form": "hanzo.submitForm",
"upload_file": "hanzo.uploadFile",
# Keyboard / mouse
"press": "hanzo.press",
"press_key": "Input.dispatchKeyEvent",
"mouse_event": "Input.dispatchMouseEvent",
"scroll": "hanzo.scroll",
"scroll_wheel": "Input.scrollWheel",
# Wait / observe
"wait_for_text": "hanzo.waitForText",
"wait_for_mutation": "hanzo.waitForMutation",
"wait_for_selector": "hanzo.waitForSelector",
"observe_start": "hanzo.observe",
"observe_read": "hanzo.observeRead",
"observe_stop": "hanzo.observeStop",
# Dialog
"dialog_accept": "hanzo.dialogAccept",
# Scripting
"evaluate": "Runtime.evaluate",
"inject_script": "hanzo.injectScript",
"inject_css": "hanzo.injectCSS",
# Cookies / storage
"cookies": "hanzo.getCookies",
"local_storage_get": "hanzo.getLocalStorage",
"local_storage_set": "hanzo.setLocalStorage",
# HTTP fetch via browser
"fetch": "hanzo.fetch",
}
return action_map.get(action, action)
def _build_params(self, data: dict) -> dict:
"""Build CDP params from HTTP request data."""
params = {}
# Keys the HTTP transport itself owns — never forward these as method params.
_ROUTING_KEYS = frozenset({
"action", "method", "client_id", "target_id", "browser", "params",
})
# Common params
if "url" in data:
params["url"] = data["url"]
if "selector" in data:
params["selector"] = data["selector"]
if "value" in data:
params["value"] = data["value"]
if "text" in data:
params["text"] = data["text"]
if "code" in data or "expression" in data:
params["expression"] = data.get("code") or data.get("expression")
if "full_page" in data or "fullPage" in data:
params["fullPage"] = data.get("full_page") or data.get("fullPage")
if "tabId" in data or "tab_id" in data:
params["tabId"] = data.get("tabId") or data.get("tab_id")
def _build_params(self, data: dict) -> dict:
"""Build CDP params from HTTP request data.
Accepts BOTH:
- flat keys at top level: {"action": "x", "tabId": 1, "target": "top"}
- nested params object: {"action": "x", "params": {"tabId": 1, "target": "top"}}
Snake_case → camelCase normalisation handled inline for the four
keys (tab_id, full_page, code, expression) that have historic aliases.
Strategy: pass through every non-routing key. The extension's
canonical dispatcher (executeMethod) is the source of truth on what
each method accepts — we don't need a whitelist here, because
unknown keys are simply ignored downstream. A whitelist creates
silent drops which is what bit us with `target`, `label`, `key`,
`intent`, `name`, etc. on the 1.9.16+ methods.
"""
params: dict = {}
# 1) Pull from nested "params" object first (per-MCP-style callers)
nested = data.get("params") if isinstance(data.get("params"), dict) else {}
for k, v in nested.items():
if k in self._ROUTING_KEYS:
continue
params[k] = v
# 2) Then overlay flat top-level keys (per-curl-style callers).
# Top-level wins so explicit overrides are honoured.
for k, v in data.items():
if k in self._ROUTING_KEYS:
continue
params[k] = v
# 3) Snake_case aliases → camelCase (historical compatibility)
if "tab_id" in params and "tabId" not in params:
params["tabId"] = params.pop("tab_id")
if "full_page" in params and "fullPage" not in params:
params["fullPage"] = params.pop("full_page")
if "code" in params and "expression" not in params:
params["expression"] = params.pop("code")
return params