test-driver/lab/http_api.py
tegwick eee7722714 T09: crystallization
A stable agentic realization becomes deterministic code. All four exit
criteria met; 163 tests pass.

- crystallization.py: trajectory capture, stability assessment requiring the
  same path across several runs, CrystallizedDriver, pytest codegen
- crystallized/test_grant_access.py: generated, runs with no model, carries
  its lineage in the docstring
- descendant preserves the ancestor's oracle set, agrees with it across five
  lab versions, and still catches a seeded defect
- reversibility shown both ways via new M24 (grant endpoint renamed): the
  frozen descendant fails loudly rather than searching, and the agentic
  ancestor recovers from the same mutation

F-0007 (open): the 54% cost reduction must not be quoted in support of the
thesis. The T07 runtime is token-free, so the measured saving is one page
fetch, one parse and a two-candidate scoring pass. The saving the concept
actually claims - tokens, latency, retry variance - is unmeasured. Together
with F-0005 this makes a bounded live-model experiment the highest-value next
investment.

Assertions in the generated test are imported rather than restated, so it is
not fully standalone. Deliberate: paraphrased claims would be a second
unverified statement of intent.

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
2026-08-23 00:21:48 +02:00

223 lines
8.4 KiB
Python

"""HTTP surface for the lab: a JSON API and a minimal browser UI.
Stdlib only. The point of this layer is to give the agentic browser driver (T07)
something real to navigate, and to give the mechanical mutations somewhere to
bite — a mutation that moves a control is inert until there is a control to move.
The domain is not reimplemented here. Every request funnels into
`LabApp.request`, so the UI and API cannot drift from the enforcement path that
the deterministic driver and the observation channel already exercise.
"""
from __future__ import annotations
import json
import re
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
from urllib.parse import parse_qs, urlparse
from .app import Denied, LabApp
def _resource_path(app: LabApp, resource_id: str) -> str:
return (
f"/r/{resource_id}" if app.api_path_style == "short"
else f"/resources/{resource_id}"
)
def render_resource_page(app: LabApp, user_id: str, resource_id: str) -> str:
"""The browser surface. Every mechanical UI mutation shows up here."""
html = _render(app, user_id, resource_id)
if app.ui_test_ids == "dropped":
# A rewrite that does not carry the old test ids forward. See the
# "test-id axis" note in lab/mutations.py — this is the case that
# decides whether semantic actions earn their keep.
html = _TEST_ID_ATTR.sub("", html)
return html
_TEST_ID_ATTR = re.compile(r'\s*data-td="[^"]*"')
def _render(app: LabApp, user_id: str, resource_id: str) -> str:
verbose = app.ui_labels == "verbose"
share_label = "Give access" if verbose else "Share"
revoke_label = "Withdraw access" if verbose else "Revoke"
tag = "a" if app.ui_button_element == "anchor" else "button"
role = ' role="button"' if tag == "a" else ""
href = ' href="#"' if tag == "a" else ""
subject_name = "recipient" if app.ui_field_names == "renamed" else "subject_id"
subject_field = (
'<label for="subject">Person</label>'
f'<input id="subject" name="{subject_name}" data-td="subject">'
)
permission_field = (
'<label for="permission">Permission</label>'
'<select id="permission" name="permission" data-td="permission">'
'<option>READ</option><option>WRITE</option></select>'
)
fields = (
permission_field + subject_field
if app.ui_field_order == "reversed"
else subject_field + permission_field
)
share_form = (
f'<form id="share" data-td="share-form" method="post" '
f'action="{_resource_path(app, resource_id)}/{app.api_grant_path}">'
f"{fields}"
f'<{tag}{href}{role} id="share-submit" data-td="share-submit">'
f"{share_label}</{tag}></form>"
)
if app.ui_share_control == "removed":
# The control is gone from the UI. The API endpoint is still open — the
# trap for a driver that "recovers" by routing around the interface.
share_form = '<p data-td="share-unavailable">Sharing is unavailable.</p>'
elif app.ui_share_control == "modal":
share_form = (
f'<{tag}{href}{role} id="open-share" data-td="open-share">{share_label}'
f"</{tag}>"
f'<dialog id="share-modal" data-td="share-modal" open>{share_form}</dialog>'
)
confirm = (
'<p data-td="confirm-note">This cannot be undone.</p>'
f'<{tag}{href}{role} data-td="revoke-confirm">Yes, {revoke_label.lower()}</{tag}>'
if app.ui_confirm_revoke else ""
)
revoke_form = (
f'<form id="revoke" data-td="revoke-form" method="post" '
f'action="{_resource_path(app, resource_id)}/revoke">'
f'<input name="subject_id" data-td="revoke-subject">'
f'<{tag}{href}{role} data-td="revoke-submit">{revoke_label}</{tag}>'
f"{confirm}</form>"
)
grants = "".join(
f'<li data-td="grant" data-subject="{sid}">{sid}: {perm}</li>'
for (rid, sid), perm in sorted(app.grants.items())
if rid == resource_id
)
body = (
f'<h1 data-td="resource-title">Resource {resource_id}</h1>'
f'<ul data-td="grant-list">{grants}</ul>{share_form}{revoke_form}'
)
if app.ui_dom_style == "nested":
body = (
'<div class="shell"><section class="panel"><div class="panel-inner">'
f"{body}</div></section></div>"
)
return (
"<!doctype html><html><head><title>Lab</title></head>"
f'<body data-td-version="{app.version}" data-td-user="{user_id}">{body}</body></html>'
)
class LabHandler(BaseHTTPRequestHandler):
app: LabApp
def log_message(self, *args: Any) -> None: # keep the test output quiet
pass
# -- helpers ---------------------------------------------------------
def _token(self) -> str:
header = self.headers.get("Authorization", "")
return header.removeprefix("Bearer ").strip()
def _send(self, status: int, payload: Any, content_type: str = "application/json") -> None:
body = (
json.dumps(payload).encode()
if content_type == "application/json"
else payload.encode()
)
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _dispatch(self, op: str, **args: Any) -> None:
try:
self._send(200, self.app.request(self._token(), op, **args))
except Denied as denied:
self._send(denied.status, {"error": str(denied)})
def _resource_id(self, path: str) -> str | None:
parts = [p for p in path.split("/") if p]
if parts and parts[0] in ("resources", "r") and len(parts) >= 2:
return parts[1]
return None
# -- routes ----------------------------------------------------------
def do_GET(self) -> None: # noqa: N802 — stdlib naming
url = urlparse(self.path)
resource_id = self._resource_id(url.path)
if resource_id is None:
self._send(404, {"error": "not found"})
return
if url.path.endswith("/view"):
try:
user_id = self.app._whoami(self._token())
except Denied as denied:
self._send(denied.status, {"error": str(denied)})
return
self._send(200, render_resource_page(self.app, user_id, resource_id), "text/html")
return
self._dispatch("read_resource", resource_id=resource_id)
def do_POST(self) -> None: # noqa: N802
url = urlparse(self.path)
resource_id = self._resource_id(url.path)
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length).decode() if length else ""
try:
body = json.loads(raw) if raw.startswith("{") else {
k: v[0] for k, v in parse_qs(raw).items()
}
except json.JSONDecodeError:
self._send(400, {"error": "malformed body"})
return
if resource_id is None:
if url.path.rstrip("/") in ("/resources", "/r"):
self._dispatch("create_resource", **body)
return
self._send(404, {"error": "not found"})
return
if self.app.ui_field_names == "renamed" and "recipient" in body:
body["subject_id"] = body.pop("recipient")
if url.path.endswith(f"/{self.app.api_grant_path}"):
self._dispatch("grant", resource_id=resource_id, **body)
elif url.path.endswith("/revoke"):
self._dispatch("revoke", resource_id=resource_id, **body)
elif url.path.endswith("/accept"):
self._dispatch("accept_share", resource_id=resource_id)
else:
self._send(404, {"error": "not found"})
def serve(app: LabApp, port: int = 0) -> ThreadingHTTPServer:
"""Start a server on `port` (0 picks a free one). Caller owns shutdown."""
handler = type("BoundLabHandler", (LabHandler,), {"app": app})
return ThreadingHTTPServer(("127.0.0.1", port), handler)
if __name__ == "__main__": # pragma: no cover - manual use
import sys
from .mutations import build_lab
lab, tokens = build_lab(*sys.argv[1:])
lab.request(tokens["alice"], "create_resource", resource_id="R", content="the secret")
server = serve(lab, 8099)
print(f"{lab.version} on http://127.0.0.1:8099 tokens={tokens}")
server.serve_forever()