T07: agentic realization over a stdlib browser surface
Two decisions taken with the operator: stdlib HTML driver instead of Playwright (F-0004), and a deterministic discovery runtime instead of a live model. Both sit behind interfaces so the alternatives drop in later. - html.py: stdlib DOM parse and query - agentic.py: DiscoveryRuntime (agentic arm, ignores data-td by construction) and RecordedSelectorRuntime (control arm, uses the strongest identifier the page offers) - browser.py: per-actor sessions over real HTTP, constructed per call so no actor inherits another's connection state - cost/nondeterminism metrics recorded from the first run F-0005 (CONCEPT_DRIFT): the H-001 result is a narrowing. Where test ids are preserved, discovery 9/9 and recorded selectors 9/9 - the semantic action buys nothing. Where they are dropped, discovery 2/3 and recorded 0/3. The concept model presents semantic actions as generally superior; the evidence says conditionally superior. M21 and M22 added mid-task: the deciding side of the axis was N=1. M22 (field names renamed) defeats the heuristic and is the first concrete evidence that a live model would add capability, not just cost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1629012@bnt-lap001 Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
This commit is contained in:
parent
925ff2dd91
commit
44faf3de8e
23 changed files with 1008 additions and 9 deletions
156
src/testdriver/browser.py
Normal file
156
src/testdriver/browser.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue