search-with-proof group 4 + image: board, spend reservation, coordinator, search-task image

Board as immutable versioned blobs (coordinator sole writer); budget reserve/settle
before model calls; per-task and run caps; coordinator loop over an injected
executor; batch.py accepts a fixed allow-list of search variables; XSS browser
oracle; ousast-search-task image (Python, Node/TypeScript, PHP, Java 17, sqlite,
bwrap, chromium) built by CI. 103 tests on the host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-05 14:26:43 +02:00
co-authored by Claude Opus 5.5
parent 6394ee473d
commit 4b38239f68
17 changed files with 1120 additions and 20 deletions
+3
View File
@@ -83,6 +83,9 @@ SPEC_OWNED = {
# Reachable modules that stand alone: their own CLI subcommand or external protocol, off the scan path.
STANDALONE = {
"search.coordinator": (
"search-with-proof Req 1/2/4: injected search executor and offline board checkpoint inspector; agent worker pending"
),
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
+67 -1
View File
@@ -10,6 +10,7 @@ failure is `AccountError` (the task is `failed` and the run starts nothing furth
from __future__ import annotations
from collections.abc import Mapping
from decimal import Decimal
from typing import Any, Protocol
from ..model.endpoint import Prices
@@ -251,5 +252,70 @@ class MeteredEmbeddingClient:
__all__ = [
"AccountError", "BudgetExhausted", "EmbeddingClient", "MeteredClient", "MeteredEmbeddingClient", "UnmeteredUsage", "cost_of",
"is_account_error", "prices_from",
"is_account_error", "prices_from", "SpendBudget", "SpendReservation",
] # fmt: skip
class SpendBudget:
"""Thread-safe pre-call admission. Share one instance across concurrent callers.
A caller must supply a genuine upper bound, including all tool-loop calls.
Reservations remain charged until settled or explicitly released on failure.
Existing MeteredClient callers retain their historical post-call metering.
"""
def __init__(self, cap: float, *, spent: float = 0.0) -> None:
from threading import Lock
self._lock = Lock()
self._cap = self._amount(cap)
self._spent = self._amount(spent)
if self._spent > self._cap:
raise ValueError("spend exceeds cap")
self._reserved = Decimal(0)
@staticmethod
def _amount(value: float) -> Decimal:
amount = Decimal(str(value))
if not amount.is_finite() or amount < 0:
raise ValueError("cost must be finite and nonnegative")
return amount
@property
def spent(self) -> float:
with self._lock:
return float(self._spent)
@property
def reserved(self) -> float:
with self._lock:
return float(self._reserved)
def reserve(self, max_cost: float) -> SpendReservation:
amount = self._amount(max_cost)
with self._lock:
if self._spent + self._reserved + amount > self._cap:
raise BudgetExhausted("spend reservation exceeds cap", usd=float(self._spent), calls=0)
self._reserved += amount
return SpendReservation(self, amount)
class SpendReservation:
def __init__(self, budget: SpendBudget, amount: Decimal) -> None:
self._budget, self._amount, self._active = budget, amount, True
def settle(self, actual: float) -> None:
amount = self._budget._amount(actual)
with self._budget._lock:
if not self._active:
raise ValueError("reservation already closed")
if amount > self._amount:
# Retain the reservation: a broken upper bound must fail closed.
raise ValueError("actual cost exceeds reservation")
self._budget._reserved -= self._amount
self._budget._spent += amount
self._active = False
def release(self) -> None:
self.settle(0)
+114
View File
@@ -0,0 +1,114 @@
"""Coordinator-owned, content-addressed search checkpoints in the plane memory store.
Workers receive snapshots and return proposals; they never receive this writer or
store credentials. Persist the returned head in the host's run checkpoint.
"""
from __future__ import annotations
import copy
import hashlib
import json
import re
import threading
from typing import Any
import yaml
from ..plane.memory import MemoryStore
class Board:
def __init__(self, store: MemoryStore, search_id: str, *, coordinator: str, max_bytes: int = 262144) -> None:
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", search_id) or not coordinator or max_bytes < 256:
raise ValueError("invalid board configuration")
self.store, self.coordinator, self.max_bytes = store, coordinator, max_bytes
self.prefix = "boards/" + hashlib.sha256(search_id.encode()).hexdigest()
self._lock = threading.RLock()
self.head: str | None = None
self._state: dict[str, Any] = dict(
search_id=search_id, version=0, previous=None, facts=[], intents=[], hints=[], end_reason=None, checkpoint={}
)
@property
def state(self) -> dict[str, Any]:
with self._lock:
return copy.deepcopy(self._state)
@classmethod
def resume(cls, store: MemoryStore, search_id: str, head: str, *, coordinator: str, max_bytes: int = 262144) -> Board:
board = cls(store, search_id, coordinator=coordinator, max_bytes=max_bytes)
data = store.get_blob(board.prefix, head)
if data is None or len(data) > max_bytes:
raise ValueError("missing or oversized board checkpoint")
state = json.loads(data)
if state["search_id"] != search_id:
raise ValueError("checkpoint belongs to another search")
board._validate(state)
board._state, board.head = state, head
return board
def commit(self, *, writer: str, **changes: Any) -> str:
with self._lock:
if writer != self.coordinator:
raise PermissionError("only the coordinator may write the board")
if self._state["end_reason"] is not None:
raise ValueError("search already ended")
if changes.keys() - {"facts", "intents", "hints", "end_reason", "checkpoint"}:
raise ValueError("unknown board fields")
state = {**self.state, **copy.deepcopy(changes), "version": self._state["version"] + 1, "previous": self.head}
self._validate(state)
data = json.dumps(state, sort_keys=True, allow_nan=False).encode()
if len(data) > self.max_bytes:
raise ValueError("board size cap exceeded")
head = self.store.put_blob(self.prefix, data)
self._state, self.head = state, head
return head
@staticmethod
def _validate(state: dict[str, Any]) -> None:
if state["end_reason"] not in (None, "goal_met", "budget_spent", "exhausted"):
raise ValueError("invalid end reason")
ids: dict[str, set[str]] = {}
for kind in ("facts", "intents", "hints"):
ids[kind] = set()
for row in state[kind]:
required = ("id", "step", "task_id", "description" if kind == "intents" else "text")
if any(not isinstance(row.get(key), str) or not row[key] for key in required):
raise ValueError("entry requires text and writer provenance")
if row["id"] in ids[kind]:
raise ValueError("duplicate board id")
ids[kind].add(row["id"])
for fact in state["facts"]:
if not isinstance(fact.get("evidence_refs"), list) or not fact["evidence_refs"]:
raise ValueError("facts require evidence references")
if any(not isinstance(ref, str) or not ref for ref in fact["evidence_refs"]):
raise ValueError("invalid evidence reference")
if not isinstance(fact.get("from_intents"), list) or set(fact["from_intents"]) - ids["intents"]:
raise ValueError("unknown source intent")
for intent in state["intents"]:
if not isinstance(intent.get("from_facts"), list) or set(intent["from_facts"]) - ids["facts"]:
raise ValueError("unknown source fact")
if intent.get("status") not in ("open", "claimed", "concluded"):
raise ValueError("invalid intent status")
if intent["status"] == "claimed" and not intent.get("claimant"):
raise ValueError("claimed intent requires claimant")
if intent["status"] == "open" and intent.get("claimant") is not None:
raise ValueError("open intent cannot have claimant")
def snapshot(self, *, max_bytes: int = 32768) -> str:
"""Valid YAML even when truncated; explicit omission counts, never a cut string."""
if max_bytes < 128:
raise ValueError("snapshot cap must be at least 128 bytes")
state = self.state
view = {key: state[key] for key in ("version", "end_reason", "facts", "intents", "hints")}
view["omitted"] = dict.fromkeys(("facts", "intents", "hints"), 0)
while True:
text = str(yaml.safe_dump(view, sort_keys=False, allow_unicode=True))
if len(text.encode()) <= max_bytes:
return text
kind = next((k for k in ("facts", "intents", "hints") if view[k]), None)
if kind is None:
raise ValueError("snapshot cap too small")
view[kind].pop(0)
view["omitted"][kind] += 1
+37
View File
@@ -0,0 +1,37 @@
"""Search ceilings carried to every executor; no model or transport dependencies."""
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class SearchBudget:
spend_usd: float = 0.50
max_call_usd: float = 0.05
reason_rounds: int = 4
intents_per_round: int = 3
demonstrations: int = 3
tasks: int = 32
retries: int = 1
memory_bytes: int = 5 * 1024**3
disk_bytes: int = 2 * 1024**3
task_wall_seconds: float = 900
wall_seconds: float = 4 * 3600
cleanup_seconds: float = 30
def __post_init__(self) -> None:
for name, value in vars(self).items():
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError(f"invalid {name}")
if value < 0 or (value == 0 and name not in ("retries", "spend_usd")):
raise ValueError(f"invalid {name}")
if name in (
"reason_rounds",
"intents_per_round",
"demonstrations",
"tasks",
"retries",
"memory_bytes",
"disk_bytes",
) and not isinstance(value, int):
raise ValueError(f"{name} must be an integer")
+348
View File
@@ -0,0 +1,348 @@
"""Host-only board driver. Executors own task isolation and enforce supplied limits.
Reason/explore workers are group 5. They return proposals, never board writes.
Only the trusted verify executor may return a demonstrated outcome. An interrupted
call is conservatively charged its reservation on resume, never silently free.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import time
from collections.abc import Callable
from dataclasses import asdict, dataclass, replace
from importlib import import_module
from pathlib import Path
from typing import Any, Protocol
from ..plane.budget import BudgetExhausted, SpendBudget
from ..plane.memory import open_store
from .board import Board
from .budget import SearchBudget
@dataclass(frozen=True)
class SearchTask:
id: str
search_id: str
step: str
snapshot: str
payload: dict[str, Any]
limits: dict[str, Any]
lane: str = "ax"
def __post_init__(self) -> None:
for value in (self.id, self.search_id):
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", value):
raise ValueError("invalid search task identifier")
if self.step not in ("reason", "explore", "verify") or self.lane not in ("ax", "kind", "docker"):
raise ValueError("invalid search task step or lane")
class Executor(Protocol):
def __call__(self, task: SearchTask) -> dict[str, Any]:
"""Finish/clean up within limits; report actual cost, including failed calls.
A missing cost on a model task charges its entire reservation. Model
workers must reserve each call against limits.max_call_usd (the task's
total allowance) using plane.budget.SpendBudget. Exceptions are untrusted
diagnostics and must never be copied into the board.
"""
...
class Coordinator:
def __init__(
self,
board: Board,
executor: Executor,
*,
budget: SearchBudget | None = None,
clock: Callable[[], float] = time.time,
push_budget: SpendBudget | None = None,
) -> None:
self.board, self.executor = board, executor
self.budget, self.clock, self.push_budget = budget or SearchBudget(), clock, push_budget
self.state = board.state
self.progress = self.state["checkpoint"] or dict(
started_at=clock(),
spent_usd=0.0,
tasks=0,
rounds=0,
retries=0,
demonstrations=0,
pending=None,
verified=[],
failures=[],
)
self.meter = SpendBudget(self.budget.spend_usd, spent=self.progress["spent_usd"])
def _save(self) -> None:
self.progress["wall_seconds"] = max(0, self.clock() - self.progress["started_at"])
self.state["checkpoint"] = self.progress
self.board.commit(
writer=self.board.coordinator, **{k: self.state[k] for k in ("facts", "intents", "hints", "end_reason", "checkpoint")}
)
def _end(self, reason: str) -> dict[str, Any]:
self.state["end_reason"] = reason
self._save()
return self.board.state
def _remaining(self) -> float:
return float(self.budget.wall_seconds - max(0, self.clock() - self.progress["started_at"]))
def _call(self, step: str, payload: dict[str, Any]) -> dict[str, Any]:
lane = "ax"
while True:
remaining = self._remaining()
if remaining <= self.budget.cleanup_seconds or self.progress["tasks"] >= self.budget.tasks:
raise BudgetExhausted("task or wall budget exhausted", usd=self.meter.spent, calls=self.progress["tasks"])
maximum = self.budget.max_call_usd if step != "verify" else 0
reservation = self.meter.reserve(maximum)
push_reservation = None
try:
if self.push_budget is not None:
push_reservation = self.push_budget.reserve(maximum)
except BudgetExhausted:
reservation.release()
raise
task_id = "task-" + str(self.progress["tasks"] + 1)
limits = asdict(self.budget)
limits["task_wall_seconds"] = min(self.budget.task_wall_seconds, remaining - self.budget.cleanup_seconds)
task = SearchTask(task_id, self.state["search_id"], step, self.board.snapshot(), payload, limits, lane)
self.progress["tasks"] += 1
self.progress["pending"] = {"id": task_id, "step": step, "payload": payload}
# A crash after this durable write may have spent the whole allowance.
self.progress["spent_usd"] = self.meter.spent + maximum
if step == "explore":
intent = next(i for i in self.state["intents"] if i["id"] == payload["intent_id"])
intent.update(status="claimed", claimant=task_id)
self._save()
task = replace(task, snapshot=self.board.snapshot())
dispatched_at = self.clock()
try:
result = self.executor(task)
except Exception:
# Includes cleanup failures: never launch more work after a task
# may have leaked. The persisted reservation remains conservative.
reservation.settle(maximum)
if push_reservation is not None:
push_reservation.settle(maximum)
raise
if not isinstance(result, dict):
result = {"status": "invalid_result"}
actual = result.get("cost_usd", maximum)
try:
reservation.settle(actual)
if push_reservation is not None:
push_reservation.settle(actual)
except (ValueError, TypeError, ArithmeticError):
# Charge the maximum and stop on a violated executor contract.
if reservation._active:
reservation.settle(maximum)
if push_reservation is not None and push_reservation._active:
push_reservation.settle(maximum)
result = {"status": "invalid_cost"}
self.progress["spent_usd"] = self.meter.spent
self.progress["pending"] = None
if (
result.get("status") == "invalid_cost"
or self._remaining() <= 0
or self.clock() - dispatched_at > limits["task_wall_seconds"]
):
raise BudgetExhausted("executor exceeded cost or wall contract", usd=self.meter.spent, calls=self.progress["tasks"])
result = {**result, "task_id": task_id}
# Persist results together with their effects in run(), not a "done"
# checkpoint which could lose the result if the host then crashes.
if result.get("status") in ("sandbox_failure", "oom", "instrument_failure") and self.progress["retries"] < self.budget.retries:
self.progress["retries"] += 1
lane = "docker"
continue
return result
def run(self) -> dict[str, Any]:
if self.state["end_reason"]:
return self.board.state
pending = self.progress.get("pending")
if pending:
# Do not duplicate a possibly paid operation. Recover the claim as a
# recorded failure; the next reason task can propose another direction.
self.progress["failures"].append({"task_id": pending["id"], "reason": "interrupted"})
for intent in self.state["intents"]:
if intent.get("claimant") == pending["id"]:
intent.update(status="concluded", failure="interrupted")
if pending["step"] == "verify":
self.progress["verified"].append(pending["payload"]["fact_id"])
self.progress["pending"] = None
self._save()
while True:
if self._remaining() <= self.budget.cleanup_seconds:
return self._end("budget_spent")
try:
intent = next((i for i in self.state["intents"] if i["status"] == "open"), None)
demo = next((f for f in self.state["facts"] if f.get("demo") and f["id"] not in self.progress["verified"]), None)
if demo is not None:
if self.progress["demonstrations"] >= self.budget.demonstrations:
return self._end("budget_spent")
self.progress["demonstrations"] += 1
result = self._call("verify", {"fact_id": demo["id"], "demo": demo["demo"]})
self.progress["verified"].append(demo["id"])
refs = result.get("evidence_refs")
if result.get("status") == "ok" and result.get("outcome") == "demonstrated" and self._refs(refs):
self.state["facts"].append(
dict(
id="verified-" + result["task_id"],
text="Differential demonstrated",
evidence_refs=refs,
from_intents=demo["from_intents"],
step="verify",
task_id=result["task_id"],
)
)
return self._end("goal_met")
self.progress["failures"].append({"task_id": result["task_id"], "reason": "not_demonstrated"})
elif intent is not None:
result = self._call("explore", {"intent_id": intent["id"], "description": intent["description"]})
intent["status"] = "concluded"
fact = result.get("fact")
if (
result.get("status") == "ok"
and isinstance(fact, dict)
and isinstance(fact.get("text"), str)
and fact["text"]
and self._refs(fact.get("evidence_refs"))
):
entry = dict(
id="fact-" + result["task_id"],
text=fact["text"],
evidence_refs=fact["evidence_refs"],
from_intents=[intent["id"]],
step="explore",
task_id=result["task_id"],
)
if isinstance(fact.get("demo"), str) and fact["demo"]:
entry["demo"] = fact["demo"]
self.state["facts"].append(entry)
else:
intent["failure"] = "invalid_result" if result.get("status") == "ok" else result.get("status", "execution_failure")
else:
if self.progress["rounds"] >= self.budget.reason_rounds:
return self._end("budget_spent")
self.progress["rounds"] += 1
result = self._call("reason", {})
proposals = result.get("intents", [])
if result.get("status") != "ok" or not isinstance(proposals, list) or len(proposals) > self.budget.intents_per_round:
return self._end("exhausted")
known = {f["id"] for f in self.state["facts"]}
descriptions = {i["description"] for i in self.state["intents"]}
added = 0
for proposal in proposals:
if not isinstance(proposal, dict):
continue
description, refs = proposal.get("description"), proposal.get("from_facts", [])
if not isinstance(description, str) or not description.strip() or description in descriptions:
continue
if not isinstance(refs, list) or any(not isinstance(ref, str) or ref not in known for ref in refs):
continue
added += 1
descriptions.add(description)
self.state["intents"].append(
dict(
id=result["task_id"] + "-intent-" + str(added),
description=description,
from_facts=refs,
status="open",
claimant=None,
step="reason",
task_id=result["task_id"],
)
)
if not added:
return self._end("exhausted")
self._save()
except BudgetExhausted:
return self._end("budget_spent")
@staticmethod
def _refs(refs: Any) -> bool:
return isinstance(refs, list) and bool(refs) and all(isinstance(ref, str) and bool(ref) for ref in refs)
class BatchExecutor:
"""Adapter to the existing AX/VM transfer and checkpoint protocol.
Pass AXLane/DockerLane instances and a trusted worker command. No default
worker is invented here: reason/explore entrypoints belong to group 5.
The coordinator owns retries, so batch performs exactly one attempt.
"""
def __init__(self, *, image: str, command: tuple[str, ...], output: Path, lanes: dict[str, Any]) -> None:
batch = import_module("benchmarks.ax.batch")
batch.validate_image(image)
if not command:
raise ValueError("worker command required")
self.image, self.command, self.output, self.lanes = image, command, output, lanes
def __call__(self, task: SearchTask) -> dict[str, Any]:
import copy
batch = import_module("benchmarks.ax.batch")
if task.lane not in self.lanes:
return {"status": "sandbox_failure", "cost_usd": 0}
data = json.dumps(asdict(task), sort_keys=True, allow_nan=False).encode()
digest = hashlib.sha256(data).hexdigest()
identity = task.search_id + "-" + task.id
item = batch.Item(
identity,
{"SPEC_URL": data},
"result.dat",
self.command,
{"SEARCH_STEP": task.step, "SEARCH_ID": task.search_id, "SEARCH_TASK_ID": task.id},
digest,
)
def validate(raw: bytes, output: Path) -> None:
if len(raw) > 262144:
raise ValueError("search result size exceeded")
record = json.loads(raw)
if not isinstance(record, dict) or record.get("status") not in (
"ok",
"sandbox_failure",
"instrument_failure",
"oom",
"execution_failure",
):
raise ValueError("invalid search result")
(output / "result.json").write_text(json.dumps(record))
workload = batch.Workload(self.image, frozenset({"SPEC_URL", "RESULT_URL"}), task.limits["task_wall_seconds"], validate)
lane = copy.copy(self.lanes[task.lane])
if hasattr(lane, "workload"):
lane.workload = workload
out = self.output / task.search_id / task.id
batch.dispatch([item], workload, out, {task.lane: lane}, lanes=(task.lane,), max_attempts=1)
record_path = out / (hashlib.sha256(identity.encode()).hexdigest() + ".json")
if not record_path.exists():
return {"status": "execution_failure", "cost_usd": 0}
record: dict[str, Any] = json.loads(record_path.read_text())
return record
def main() -> None:
"""Offline checkpoint inspection, also useful before resuming with an executor."""
parser = argparse.ArgumentParser(description="Inspect a search board checkpoint (no model calls)")
parser.add_argument("--memory", required=True)
parser.add_argument("--search-id", required=True)
parser.add_argument("--head", required=True)
args = parser.parse_args()
board = Board.resume(open_store(args.memory), args.search_id, args.head, coordinator="inspect")
print(board.snapshot(), end="")
if __name__ == "__main__":
main()
+100 -3
View File
@@ -11,12 +11,16 @@ import json
import os
import secrets
import selectors
import shutil
import sqlite3
import subprocess
import tempfile
from collections.abc import Callable
from dataclasses import dataclass
from html.parser import HTMLParser
from pathlib import Path
from typing import Protocol
from urllib.parse import quote
@dataclass(frozen=True)
@@ -136,18 +140,111 @@ class SSRFOracle(OutputOracle):
self.process.stdout.close()
BrowserExecutor = Callable[[str, str], bool]
class BrowserExecutor:
"""Run captured HTML with a verifier-owned alert canary.
Payloads call alert("ousast-xss"). Reflection/escaped HTML cannot set the fresh
DOM marker. A restrictive CSP disables external resources and connections;
Chromium's sandbox stays enabled and each capture uses a fresh profile.
Launch failures are unavailable execution, never a negative observation.
"""
def __init__(self, binary: str | None = None, *, timeout_seconds: float = 15) -> None:
selected = binary or shutil.which("chromium") or shutil.which("chromium-browser")
self.timeout_seconds = timeout_seconds
if not selected:
raise OSError("headless chromium unavailable")
self.binary: str = selected
def __call__(self, document: str, nonce: str) -> bool:
marker = secrets.token_hex(24)
# Neither the marker nor its attribute appears in app output. The bootstrap
# source does appear in dump-dom, so inspect an actual DOM attribute only.
attribute = "data-proof-" + secrets.token_hex(12)
prelude = (
'<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; '
"script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\">"
'<script>window.alert=function(value){if(value==="ousast-xss"||value==='
+ json.dumps(nonce)
+ "){document.documentElement.setAttribute("
+ json.dumps(attribute)
+ ","
+ json.dumps(marker)
+ ");}};</script>"
)
with tempfile.TemporaryDirectory(prefix="ousast-browser-") as profile:
# HTML can navigate even with CSP. A private network namespace, not
# browser flags, is the no-egress boundary. Expose runtime only.
sandbox = ["bwrap", "--unshare-all", "--die-with-parent", "--new-session"]
for path in ("/usr", "/bin", "/lib", "/lib64"):
if Path(path).exists():
sandbox += ["--ro-bind", path, path]
sandbox += [
"--dev",
"/dev",
"--proc",
"/proc",
"--tmpfs",
"/tmp",
"--bind",
profile,
"/profile",
"--setenv",
"HOME",
"/profile",
"--",
]
try:
done = subprocess.run(
[
*sandbox,
self.binary,
"--headless",
"--dump-dom",
"--disable-gpu",
"--disable-background-networking",
"--disable-extensions",
"--no-first-run",
"--no-default-browser-check",
"--disable-sync",
"--user-data-dir=/profile",
"--virtual-time-budget=1000",
"data:text/html;charset=utf-8," + quote(prelude + document),
],
capture_output=True,
text=True,
timeout=self.timeout_seconds,
env={"PATH": "/usr/bin:/bin", "HOME": profile},
)
except subprocess.SubprocessError as exc:
raise OSError("headless chromium failed") from exc
if done.returncode or "<html" not in done.stdout.lower():
raise OSError("headless chromium did not produce a document: " + done.stderr[-500:])
class Marker(HTMLParser):
seen = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "html" and (attribute, marker) in attrs:
self.seen = True
parser = Marker()
parser.feed(done.stdout)
return parser.seen
Browser = Callable[[str, str], bool]
class XSSOracle(OutputOracle):
def __init__(self, executor: BrowserExecutor) -> None:
def __init__(self, executor: Browser) -> None:
self.executor = executor
def capture(self, output: str) -> None:
self.seen |= self.executor(output, self.canary.nonce)
def oracle_for(family: str, browser: BrowserExecutor | None = None) -> Oracle | None:
def oracle_for(family: str, browser: Browser | None = None) -> Oracle | None:
factories = {"path": PathOracle, "sql": SQLOracle, "command": CommandOracle, "ssrf": SSRFOracle}
if family == "xss":
return XSSOracle(browser) if browser else None