From 1a95ab5b679282645b7094e09b8e1af109e0f60f Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Sun, 25 Jan 2026 10:11:48 -0800 Subject: [PATCH] Pagecontrol hook for evaluateJs --- sentience/agent_runtime.py | 18 +++++- sentience/captcha.py | 8 ++- .../unit/test_captcha_context_page_control.py | 56 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_captcha_context_page_control.py diff --git a/sentience/agent_runtime.py b/sentience/agent_runtime.py index 6129e13..8582868 100644 --- a/sentience/agent_runtime.py +++ b/sentience/agent_runtime.py @@ -70,7 +70,13 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from .captcha import CaptchaContext, CaptchaHandlingError, CaptchaOptions, CaptchaResolution +from .captcha import ( + CaptchaContext, + CaptchaHandlingError, + CaptchaOptions, + CaptchaResolution, + PageControlHook, +) from .failure_artifacts import FailureArtifactBuffer, FailureArtifactsOptions from .models import ( EvaluateJsRequest, @@ -479,8 +485,18 @@ def _build_captcha_context(self, snapshot: Snapshot, source: str) -> CaptchaCont url=snapshot.url, source=source, # type: ignore[arg-type] captcha=captcha, + page_control=self._create_captcha_page_control(), ) + def _create_captcha_page_control(self) -> PageControlHook: + async def _eval(code: str) -> Any: + result = await self.evaluate_js(EvaluateJsRequest(code=code)) + if not result.ok: + raise RuntimeError(result.error or "evaluate_js failed") + return result.value + + return PageControlHook(evaluate_js=_eval) + def _emit_captcha_event(self, reason_code: str, details: dict[str, Any] | None = None) -> None: payload = { "kind": "captcha", diff --git a/sentience/captcha.py b/sentience/captcha.py index ef2b560..816e3f8 100644 --- a/sentience/captcha.py +++ b/sentience/captcha.py @@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Literal, Optional +from typing import Any, Literal, Optional from .models import CaptchaDiagnostics @@ -11,6 +11,11 @@ CaptchaSource = Literal["extension", "gateway", "runtime"] +@dataclass +class PageControlHook: + evaluate_js: Callable[[str], Awaitable[Any]] + + @dataclass class CaptchaContext: run_id: str @@ -23,6 +28,7 @@ class CaptchaContext: snapshot_path: str | None = None live_session_url: str | None = None meta: dict[str, str] | None = None + page_control: PageControlHook | None = None @dataclass diff --git a/tests/unit/test_captcha_context_page_control.py b/tests/unit/test_captcha_context_page_control.py new file mode 100644 index 0000000..5ba8af2 --- /dev/null +++ b/tests/unit/test_captcha_context_page_control.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import pytest + +from sentience.agent_runtime import AgentRuntime +from sentience.captcha import PageControlHook +from sentience.models import CaptchaDiagnostics, CaptchaEvidence, Snapshot, SnapshotDiagnostics + + +class EvalBackend: + async def eval(self, code: str): + _ = code + return "ok" + + +class MockTracer: + def __init__(self) -> None: + self.events: list[dict] = [] + self.run_id = "test-run" + + def emit(self, event_type: str, data: dict, step_id: str | None = None) -> None: + self.events.append({"type": event_type, "data": data, "step_id": step_id}) + + +def make_captcha_snapshot() -> Snapshot: + evidence = CaptchaEvidence( + iframe_src_hits=["https://www.google.com/recaptcha/api2/anchor"], + text_hits=["captcha"], + selector_hits=[], + url_hits=[], + ) + captcha = CaptchaDiagnostics( + detected=True, + provider_hint="recaptcha", + confidence=0.9, + evidence=evidence, + ) + diagnostics = SnapshotDiagnostics(captcha=captcha) + return Snapshot( + status="success", + url="https://example.com", + elements=[], + diagnostics=diagnostics, + ) + + +@pytest.mark.asyncio +async def test_captcha_context_page_control_evaluate_js() -> None: + runtime = AgentRuntime(backend=EvalBackend(), tracer=MockTracer()) + runtime.begin_step("captcha_test") + + ctx = runtime._build_captcha_context(make_captcha_snapshot(), source="gateway") + assert isinstance(ctx.page_control, PageControlHook) + + result = await ctx.page_control.evaluate_js("1+1") + assert result == "ok"