"""A browser-surface driver over the lab's HTML UI. Each actor gets its **own** session — its own base URL binding, its own credentials, its own cookie-equivalent. Nothing is shared between actors at module or class level, because a shared client is exactly how isolation breaks in practice and it never announces itself (F-0003). The driver executes a `Plan` produced by an `ActorRuntime`. It does not decide what to click and it does not decide whether the outcome was correct: the first belongs to the runtime, the second to the observer and the oracle. """ from __future__ import annotations import json import urllib.error import urllib.parse import urllib.request from dataclasses import dataclass from typing import Any from .actions import SemanticAction, Surface from .agentic import ActorRuntime, RealizationFailed from .drivers import Realization, UnsupportedAction from .html import Document from .world import Actor @dataclass(slots=True) class Session: """One actor's private connection to the surface. Never shared.""" base_url: str token: str def _open(self, method: str, path: str, body: bytes | None, content_type: str): request = urllib.request.Request( urllib.parse.urljoin(self.base_url, path), data=body, method=method, headers={ "Authorization": f"Bearer {self.token}", **({"Content-Type": content_type} if body else {}), }, ) try: with urllib.request.urlopen(request, timeout=10) as response: return response.status, response.read().decode() except urllib.error.HTTPError as error: return error.code, error.read().decode() def get(self, path: str) -> tuple[int, str]: return self._open("GET", path, None, "") def post_form(self, path: str, fields: dict[str, str]) -> tuple[int, str]: encoded = urllib.parse.urlencode(fields).encode() return self._open("POST", path, encoded, "application/x-www-form-urlencoded") def post_json(self, path: str, payload: dict[str, Any]) -> tuple[int, str]: return self._open("POST", path, json.dumps(payload).encode(), "application/json") class BrowserDriver: """Realizes semantic actions by finding and using controls on an HTML page.""" def __init__( self, base_url: str, tokens: dict[str, str], runtime: ActorRuntime, resource_id: str, ) -> None: self._base_url = base_url self._tokens = tokens self._runtime = runtime self._resource_id = resource_id self.surface = Surface( id="browser", kind="html", description="lab browser UI" ) def _session_for(self, actor: Actor) -> Session: # Constructed per call: no actor can inherit another's connection state. return Session(self._base_url, self._tokens[actor.id]) def _view_path(self) -> str: return f"/resources/{self._resource_id}/view" def realize(self, actor: Actor, action: SemanticAction) -> Realization: action.check_surface(self.surface.id) session = self._session_for(actor) status, body = session.get(self._view_path()) if status != 200: # A surface the actor cannot even load is not an adaptation problem. return Realization( self.surface.id, {"action": action.name, "stage": "navigate", "status": status}, raised=f"NavigationFailed: {status}", ) document = Document.parse(body) args = {"resource_id": self._resource_id, **dict(action.args)} try: plan = self._runtime.plan(document, action.name, args) except RealizationFailed as failure: return Realization( self.surface.id, { "action": action.name, "stage": "discovery", "runtime": getattr(self._runtime, "name", "?"), "page_bytes": len(body), }, raised=f"RealizationFailed: {failure}", ) mechanics: dict[str, Any] = { "action": action.describe(), "stage": "submit", "runtime": plan.metrics.runtime, "rationale": plan.rationale, "element_path": plan.element_path, "target": plan.action, "fields": sorted(plan.fields), "metrics": plan.metrics.as_dict(), "actor": actor.id, } status, response = session.post_form(plan.action, plan.fields) mechanics["status"] = status if status >= 400: return Realization( self.surface.id, mechanics, raised=f"Refused: {status} {response[:120]}" ) return Realization(self.surface.id, mechanics) def record_baseline_selectors(html: str) -> dict[str, str]: """Capture the control arm's selectors from a baseline page. Deliberately generous: it takes the strongest identifier the page offers. A weak control arm would make H-001 trivially true and worthless. """ document = Document.parse(html) recordings: dict[str, str] = {} for form in document.forms(): test_id = form.attrs.get("data-td", "") target = form.attrs.get("action", "") if not test_id: continue if target.endswith("/grant"): recordings["grant_access"] = test_id elif target.endswith("/revoke"): recordings["revoke_access"] = test_id return recordings