"""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