feat: add page.stealth_evaluate() for undetectable JS execution (#108)

Extract CDP isolated world classes from humanize layer into standalone
stealth_eval module. Attach page.stealth_evaluate(expression) to every
page automatically — runs JS in a CDP isolated world with clean
Error.stack traces and full variable isolation from main world JS.
This commit is contained in:
CloakHQ
2026-04-06 04:38:49 +02:00
parent 216a7d6a6a
commit a32d7c4eae
11 changed files with 639 additions and 262 deletions
+20
View File
@@ -138,6 +138,10 @@ def launch(
cfg = resolve_config(human_preset, human_config)
patch_browser(browser, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_browser_stealth_eval
patch_browser_stealth_eval(browser, is_async=False)
return browser
@@ -227,6 +231,10 @@ async def launch_async( # noqa: C901
cfg = resolve_config(human_preset, human_config)
patch_browser_async(browser, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_browser_stealth_eval
patch_browser_stealth_eval(browser, is_async=True)
return browser
@@ -344,6 +352,10 @@ def launch_persistent_context(
cfg = resolve_config(human_preset, human_config)
patch_context(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=False)
return context
@@ -463,6 +475,10 @@ async def launch_persistent_context_async(
cfg = resolve_config(human_preset, human_config)
patch_context_async(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=True)
return context
@@ -556,6 +572,10 @@ def launch_context(
cfg = resolve_config(human_preset, human_config)
patch_context(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=False)
return context
+13 -141
View File
@@ -26,6 +26,7 @@ from .scroll import scroll_to_element
from .mouse_async import AsyncRawMouse, async_human_move, async_human_click, async_human_idle
from .keyboard_async import AsyncRawKeyboard, async_human_type
from .scroll_async import async_scroll_to_element
from ..stealth_eval import _SyncIsolatedWorld, _AsyncIsolatedWorld
_SELECT_ALL = "Meta+a" if sys.platform == "darwin" else "Control+a"
@@ -44,142 +45,9 @@ logger = logging.getLogger("cloakbrowser.human")
# CDP Isolated World — stealth DOM evaluation
# ============================================================================
class _SyncIsolatedWorld:
"""Manages a CDP isolated execution context for DOM reads (sync).
Produces clean Error.stack traces (no 'eval at evaluate :302:')
and is invisible to querySelector monkey-patches in the main world.
Context ID is invalidated on navigation and auto-recreated on next call.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = self._page.context.new_cdp_session(self._page)
return self._cdp
def _create_world(self) -> int:
cdp = self._ensure_cdp()
tree = cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
self._create_world()
for attempt in range(2):
try:
result = self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
self._create_world()
continue
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
self._create_world()
except Exception:
return None
continue
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return self._ensure_cdp()
class _AsyncIsolatedWorld:
"""Manages a CDP isolated execution context for DOM reads (async).
Same as _SyncIsolatedWorld but uses await for all CDP calls.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
async def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = await self._page.context.new_cdp_session(self._page)
return self._cdp
async def _create_world(self) -> int:
cdp = await self._ensure_cdp()
tree = await cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = await cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
async def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
await self._create_world()
for attempt in range(2):
try:
result = await self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
await self._create_world()
continue
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
await self._create_world()
except Exception:
return None
continue
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
async def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return await self._ensure_cdp()
# _SyncIsolatedWorld and _AsyncIsolatedWorld are defined in
# cloakbrowser.stealth_eval and imported at the top of this file.
# ============================================================================
@@ -739,10 +607,12 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
page._original = originals
page._human_cfg = cfg
# --- Stealth infrastructure ---
# --- Stealth infrastructure (reuse if already attached by stealth_eval) ---
try:
stealth = _SyncIsolatedWorld(page)
page._stealth_world = stealth
stealth = getattr(page, '_stealth_world', None)
if not isinstance(stealth, _SyncIsolatedWorld):
stealth = _SyncIsolatedWorld(page)
page._stealth_world = stealth
cdp_session = stealth.get_cdp_session()
except Exception:
stealth = None
@@ -1104,9 +974,11 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
page._original = originals
page._human_cfg = cfg
# --- Stealth infrastructure (lazy-initialized, async) ---
stealth = _AsyncIsolatedWorld(page)
page._stealth_world = stealth
# --- Stealth infrastructure (reuse if already attached by stealth_eval) ---
stealth = getattr(page, '_stealth_world', None)
if not isinstance(stealth, _AsyncIsolatedWorld):
stealth = _AsyncIsolatedWorld(page)
page._stealth_world = stealth
cdp_session_holder: list[Any] = [None] # mutable container for closure
async def _ensure_cdp() -> Any:
+271
View File
@@ -0,0 +1,271 @@
"""Stealth evaluate — run JS in a CDP isolated world.
Provides page.stealth_evaluate(expression) on every page returned by
cloakbrowser launch functions. Produces clean Error.stack traces (no
``eval at evaluate :302:`` leak) and full variable isolation from main
world JS. Context auto-recreates after navigation.
The same isolated world instances are reused by the humanize layer
(human/__init__.py) for stealth DOM queries.
"""
from __future__ import annotations
import logging
from typing import Any, Optional
logger = logging.getLogger("cloakbrowser.stealth_eval")
# ============================================================================
# Isolated world classes
# ============================================================================
class _SyncIsolatedWorld:
"""CDP isolated execution context for DOM reads (sync).
Produces clean Error.stack traces and is invisible to
querySelector monkey-patches in the main world.
Context ID is invalidated on navigation and auto-recreated.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = self._page.context.new_cdp_session(self._page)
return self._cdp
def _create_world(self) -> int:
cdp = self._ensure_cdp()
tree = cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
try:
self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to create isolated world")
return None
for attempt in range(2):
try:
result = self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
self._create_world()
continue
logger.debug("stealth_evaluate: JS exception: %s",
result["exceptionDetails"].get("text", "unknown"))
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to recreate isolated world")
return None
continue
logger.debug("stealth_evaluate: CDP evaluate failed after retry")
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return self._ensure_cdp()
class _AsyncIsolatedWorld:
"""CDP isolated execution context for DOM reads (async).
Same as _SyncIsolatedWorld but uses await for all CDP calls.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
async def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = await self._page.context.new_cdp_session(self._page)
return self._cdp
async def _create_world(self) -> int:
cdp = await self._ensure_cdp()
tree = await cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = await cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
async def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
try:
await self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to create isolated world")
return None
for attempt in range(2):
try:
result = await self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
await self._create_world()
continue
logger.debug("stealth_evaluate: JS exception: %s",
result["exceptionDetails"].get("text", "unknown"))
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
await self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to recreate isolated world")
return None
continue
logger.debug("stealth_evaluate: CDP evaluate failed after retry")
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
async def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return await self._ensure_cdp()
# ============================================================================
# Page / context / browser patching
# ============================================================================
def _patch_page_sync(page: Any) -> None:
"""Attach page.stealth_evaluate() using a sync isolated world."""
if hasattr(page, "stealth_evaluate"):
return
existing = getattr(page, "_stealth_world", None)
if isinstance(existing, _SyncIsolatedWorld):
world = existing
else:
world = _SyncIsolatedWorld(page)
page._stealth_world = world
page.stealth_evaluate = world.evaluate
def _patch_page_async(page: Any) -> None:
"""Attach page.stealth_evaluate() using an async isolated world."""
if hasattr(page, "stealth_evaluate"):
return
existing = getattr(page, "_stealth_world", None)
if isinstance(existing, _AsyncIsolatedWorld):
world = existing
else:
world = _AsyncIsolatedWorld(page)
page._stealth_world = world
page.stealth_evaluate = world.evaluate
def patch_context_stealth_eval(context: Any, *, is_async: bool = False) -> None:
"""Patch existing pages + hook new_page() for stealth_evaluate."""
if getattr(context, "_stealth_eval_patched", False):
return
context._stealth_eval_patched = True
patch_fn = _patch_page_async if is_async else _patch_page_sync
for p in context.pages:
patch_fn(p)
orig_new_page = context.new_page
if is_async:
async def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = await orig_new_page(*args, **kwargs)
patch_fn(page)
return page
else:
def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = orig_new_page(*args, **kwargs)
patch_fn(page)
return page
context.new_page = _patched_new_page
context.on("page", lambda p: patch_fn(p))
def patch_browser_stealth_eval(browser: Any, *, is_async: bool = False) -> None:
"""Patch browser factory methods for stealth_evaluate."""
patch_fn = _patch_page_async if is_async else _patch_page_sync
# Hook new_context()
orig_new_context = browser.new_context
if is_async:
async def _patched_new_context(*args: Any, **kwargs: Any) -> Any:
ctx = await orig_new_context(*args, **kwargs)
patch_context_stealth_eval(ctx, is_async=True)
return ctx
else:
def _patched_new_context(*args: Any, **kwargs: Any) -> Any:
ctx = orig_new_context(*args, **kwargs)
patch_context_stealth_eval(ctx, is_async=False)
return ctx
browser.new_context = _patched_new_context
# Hook new_page()
orig_new_page = browser.new_page
if is_async:
async def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = await orig_new_page(*args, **kwargs)
patch_context_stealth_eval(page.context, is_async=True)
patch_fn(page)
return page
else:
def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = orig_new_page(*args, **kwargs)
patch_context_stealth_eval(page.context, is_async=False)
patch_fn(page)
return page
browser.new_page = _patched_new_page