Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion sentience/agent_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion sentience/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -11,6 +11,11 @@
CaptchaSource = Literal["extension", "gateway", "runtime"]


@dataclass
class PageControlHook:
evaluate_js: Callable[[str], Awaitable[Any]]


@dataclass
class CaptchaContext:
run_id: str
Expand All @@ -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
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/test_captcha_context_page_control.py
Original file line number Diff line number Diff line change
@@ -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"
Loading