chat-browser-bridge / javascript_core.py
ifroyou's picture
Add isolated browser JavaScript, keyboard and pointer tools (#1)
6e4eb30
Raw History Blame Contribute Delete
7 kB
from __future__ import annotations
import asyncio
import json
import re
from typing import Any
from browser_core import BrowserBridgeError, BrowserManager
JAVASCRIPT_BACKEND = "playwright-page-evaluate-anonymous-cloud"
MAX_JAVASCRIPT_CHARS = 24_000
MAX_JAVASCRIPT_RESULT_BYTES = 128 * 1024
MAX_JAVASCRIPT_TIMEOUT_MS = 5_000
_FORBIDDEN_SOURCE = re.compile(
r"(?:document\s*\.\s*cookie|localStorage|sessionStorage|indexedDB|\bcaches\b|"
r"navigator\s*\.\s*credentials|\bpassword\b|\bpasscode\b|\botp\b|"
r"one[\s_-]*time[\s_-]*code|\bauthorization\b|\bbearer\b|\baccess[\s_-]*token\b|"
r"\brefresh[\s_-]*token\b|\bwebsocket\b|\beventsource\b|\bxmlhttprequest\b|"
r"\bnavigator\s*\.\s*sendBeacon\b)",
re.IGNORECASE,
)
def _source(value: str) -> str:
source = str(value or "")
if not source.strip():
raise BrowserBridgeError("JAVASCRIPT_EMPTY", "A JavaScript snippet is required.")
if len(source) > MAX_JAVASCRIPT_CHARS:
raise BrowserBridgeError(
"JAVASCRIPT_TOO_LARGE",
"JavaScript is limited to 24,000 characters in the isolated browser session.",
details={"max_characters": MAX_JAVASCRIPT_CHARS},
)
blocked = _FORBIDDEN_SOURCE.search(source)
if blocked:
raise BrowserBridgeError(
"JAVASCRIPT_SECRET_OR_NETWORK_API_BLOCKED",
"The JavaScript tool never reads browser storage, cookies, credentials, password/OTP fields, or direct network APIs.",
details={"blocked_token": blocked.group(0)[:80]},
)
return source
async def run_javascript(
manager: BrowserManager,
owner: str,
session_id: str,
script: str,
*,
user_confirmed_javascript_action: bool,
) -> dict[str, Any]:
if user_confirmed_javascript_action is not True:
raise BrowserBridgeError(
"JAVASCRIPT_ACTION_CONFIRMATION_REQUIRED",
"Confirm the exact JavaScript action on the current anonymous public cloud page before execution.",
)
source = _source(script)
session = await manager.get(owner, session_id, touch=False)
async with session.lock:
await manager.revalidate_locked_session(owner, session_id, session)
page = await manager._sync_page(session)
try:
raw = await asyncio.wait_for(
page.evaluate(
"""
async ({ source, maxBytes }) => {
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const toSafeJson = (value) => {
const seen = new WeakSet();
const json = JSON.stringify(value === undefined ? null : value, (_key, item) => {
if (typeof item === 'bigint') return item.toString();
if (typeof item === 'function' || typeof item === 'symbol') return undefined;
if (item && typeof item === 'object') {
if (seen.has(item)) return '[Circular]';
seen.add(item);
}
return item;
});
return json === undefined ? 'null' : json;
};
const originalFetch = globalThis.fetch;
const originalOpen = globalThis.open;
const originalWebSocket = globalThis.WebSocket;
const originalXhr = globalThis.XMLHttpRequest;
const originalEventSource = globalThis.EventSource;
const originalBeacon = navigator.sendBeacon;
const deny = (name) => () => { throw new Error(`${name} is unavailable in this browser JavaScript tool`); };
try {
globalThis.fetch = deny('fetch');
globalThis.open = deny('window.open');
globalThis.WebSocket = undefined;
globalThis.XMLHttpRequest = undefined;
globalThis.EventSource = undefined;
navigator.sendBeacon = deny('navigator.sendBeacon');
const run = new AsyncFunction(`"use strict"; return await (async () => { ${source}\n })();`);
const value = await run();
const resultJson = toSafeJson(value);
if (new TextEncoder().encode(resultJson).length > maxBytes) {
throw new Error(`JavaScript result exceeds ${maxBytes} bytes`);
}
return { ok: true, result_json: resultJson, result_bytes: new TextEncoder().encode(resultJson).length };
} finally {
globalThis.fetch = originalFetch;
globalThis.open = originalOpen;
globalThis.WebSocket = originalWebSocket;
globalThis.XMLHttpRequest = originalXhr;
globalThis.EventSource = originalEventSource;
navigator.sendBeacon = originalBeacon;
}
}
""",
{"source": source, "maxBytes": MAX_JAVASCRIPT_RESULT_BYTES},
),
timeout=MAX_JAVASCRIPT_TIMEOUT_MS / 1000,
)
except asyncio.TimeoutError as exc:
raise BrowserBridgeError(
"JAVASCRIPT_TIMEOUT",
"JavaScript exceeded the five-second cloud-browser limit.",
details={"timeout_ms": MAX_JAVASCRIPT_TIMEOUT_MS},
) from exc
except BrowserBridgeError:
raise
except Exception as exc:
raise BrowserBridgeError(
"JAVASCRIPT_EXECUTION_FAILED",
"JavaScript could not be completed in the isolated browser session.",
details={"error_type": type(exc).__name__},
) from exc
try:
result = json.loads(str(raw.get("result_json") or "null"))
except Exception as exc:
raise BrowserBridgeError(
"JAVASCRIPT_RESULT_INVALID",
"JavaScript returned a result that could not be safely serialized.",
) from exc
return {
"ok": True,
"status": "BROWSER_JAVASCRIPT_COMPLETED",
"session_id": session.session_id,
"url": page.url,
"result": result,
"result_bytes": int(raw.get("result_bytes") or 0),
"script_characters": len(source),
"javascript_backend": JAVASCRIPT_BACKEND,
"execution_scope": "anonymous-public-cloud-browser-session",
"desktop_input_used": False,
"user_edge_profile_used": False,
"cookies_or_profile_exposed": False,
"submitted": False,
}