search-with-proof: declarative demos, brain/executor split, kube-ax constraints

Demos are data the verifier executes (fixed build recipes, runtime+path start,
HTTP/CLI steps with captures; the canary is never referable). The agent loop with
the key never runs repository code: executor tasks without a key exchange commands
and results through presigned objects. AX capacity errors are back-pressure with at
most 2 tasks in flight; caches and builds under /workspace with a 2 GiB guard;
verifiers take one presigned archive per side. Launcher fix: private /dev in bwrap.
190 tests on the host; probe 5/5 demonstrated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-05 17:12:46 +02:00
co-authored by Claude Opus 5.5
parent 721c4b0997
commit 85f4f0b3d4
25 changed files with 3409 additions and 531 deletions
+221
View File
@@ -0,0 +1,221 @@
"""Key-free AX task entry for one declarative verification repetition."""
from __future__ import annotations
import gzip
import io
import ipaddress
import json
import math
import os
import stat
import tarfile
import tempfile
import time
import urllib.error
import urllib.request
from dataclasses import asdict
from pathlib import Path, PurePosixPath
from typing import Any, cast
from urllib.parse import urlsplit
from . import task_storage
from .demo import validate_demo
from .executor import clean_environment
from .verify import Side, SideRecord
def public_endpoint(url: str) -> str:
parts = urlsplit(url)
host = parts.hostname or ""
try:
ipaddress.ip_address(host)
is_ip = True
except ValueError:
is_ip = False
if (
parts.scheme != "https"
or not host
or is_ip
or "." not in host
or host.endswith((".localhost", ".local", ".internal"))
or parts.port not in (None, 443)
or parts.username
or parts.password
):
raise ValueError("task URL requires public HTTPS hostname")
return host
MAX_ARCHIVE_BYTES = 64 * 1024 * 1024
MAX_CHECKOUT_BYTES = 256 * 1024 * 1024
MAX_FILES = 20000
MAX_SPEC_BYTES = 70000
MAX_RESULT_BYTES = 65536
def pack_checkout(root: Path) -> bytes:
root = Path(root)
if root.is_symlink() or not root.is_dir():
raise ValueError("checkout must be a real directory")
buffer, total = io.BytesIO(), 0
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
paths = (p for p in root.rglob("*") if ".git" not in p.relative_to(root).parts)
for index, path in enumerate(sorted(paths)):
mode = path.lstat().st_mode
if index >= MAX_FILES or not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
raise ValueError("checkout contains too many files or special files")
total += path.stat().st_size if path.is_file() else 0
if total > MAX_CHECKOUT_BYTES:
raise ValueError("checkout exceeds size limit")
archive.add(path, arcname=str(path.relative_to(root)), recursive=False)
data = buffer.getvalue()
if len(data) > MAX_ARCHIVE_BYTES:
raise ValueError("archive exceeds size limit")
return data
def extract_checkout(data: bytes, destination: Path) -> None:
if len(data) > MAX_ARCHIVE_BYTES:
raise ValueError("archive exceeds size limit")
# Bound expansion before tarfile parses PAX headers or allocates member data.
with gzip.GzipFile(fileobj=io.BytesIO(data)) as compressed:
expanded = compressed.read(MAX_CHECKOUT_BYTES + MAX_FILES * 4096 + 1)
if len(expanded) > MAX_CHECKOUT_BYTES + MAX_FILES * 4096:
raise ValueError("expanded checkout exceeds size limit")
destination.mkdir(parents=True)
total, seen = 0, set()
with tarfile.open(fileobj=io.BytesIO(expanded), mode="r:") as archive:
for index, member in enumerate(archive):
path = PurePosixPath(member.name)
if (
index >= MAX_FILES
or path.is_absolute()
or ".." in path.parts
or not path.parts
or "\\" in member.name
or member.name in seen
or not (member.isfile() or member.isdir())
):
raise ValueError("unsafe checkout archive member")
seen.add(member.name)
total += member.size
if member.size < 0 or total > MAX_CHECKOUT_BYTES:
raise ValueError("expanded checkout exceeds size limit")
target = destination.joinpath(*path.parts)
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
source = archive.extractfile(member)
if source is None:
raise ValueError("missing regular checkout member")
with source, target.open("xb") as output:
remaining = member.size
while remaining:
block = source.read(min(65536, remaining))
if not block:
raise ValueError("truncated checkout member")
output.write(block)
remaining -= len(block)
target.chmod(0o755 if member.mode & 0o111 else 0o644)
if not seen:
raise ValueError("empty checkout")
print(f"checkout_bytes={total} checkout_entries={len(seen)}", flush=True)
def validate_spec(spec: Any) -> dict[str, Any]:
if not isinstance(spec, dict) or set(spec) != {"demo", "family", "timeout_seconds"}:
raise ValueError("invalid verification spec")
if len(json.dumps(spec, allow_nan=False).encode()) > MAX_SPEC_BYTES:
raise ValueError("oversized verification spec")
validate_demo(spec["demo"])
if spec["demo"]["build"]["recipe"] != "none":
raise ValueError("verifier requires executor-built archive; builds are forbidden")
if spec["family"] not in {"sql", "path", "command", "ssrf", "xss"}:
raise ValueError("invalid verification family")
timeout = spec["timeout_seconds"]
if type(timeout) is not int or not math.isfinite(timeout) or not 1 <= timeout <= 300:
raise ValueError("invalid verification timeout")
return cast(dict[str, Any], spec)
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
raise ValueError("presigned redirects refused")
def download(url: str, limit: int, *, readiness_budget: float = 60) -> bytes:
"""The first object read tolerates the task egress policy's startup delay."""
public_endpoint(url)
end = time.monotonic() + readiness_budget
delay = 1.0
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect)
while True:
try:
with opener.open(url, timeout=max(0.01, min(30, end - time.monotonic()))) as response:
data = response.read(limit + 1)
if len(data) > limit:
raise ValueError("oversized task input")
return cast(bytes, data)
except urllib.error.HTTPError as exc:
if exc.code != 403 and not 500 <= exc.code < 600:
raise OSError("task input download failed") from None
except (urllib.error.URLError, OSError):
pass
if time.monotonic() >= end:
raise OSError("task input egress not ready before download deadline") from None
time.sleep(min(delay, end - time.monotonic()))
delay = min(delay * 2, 8)
def upload(url: str, data: bytes) -> None:
public_endpoint(url)
if len(data) > MAX_RESULT_BYTES:
raise ValueError("oversized task result")
request = urllib.request.Request(url, data=data, method="PUT")
with urllib.request.build_opener(_NoRedirect).open(request, timeout=30) as response:
if not 200 <= response.status < 300:
raise ValueError("result upload failed")
def run_side(side: Side, demo: Path, family: str, timeout_seconds: int) -> SideRecord:
from openultrasast.search.verify import verify_side_task
return verify_side_task(side, demo, family, timeout_seconds)
def task_main() -> None:
if any(
value and any(word in key.upper() for word in ("KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL"))
for key, value in os.environ.items()
):
raise ValueError("verification task refuses inherited credentials")
urls = {key: os.environ[key] for key in ("VERIFY_INPUT_URL", "RESULT_URL")}
for url in urls.values():
public_endpoint(url)
scratch_limit = task_storage.configure()
os.environ.clear()
os.environ.update(clean_environment())
os.environ["OUSAST_SCRATCH_BYTES"] = str(scratch_limit)
archive = download(urls["VERIFY_INPUT_URL"], MAX_ARCHIVE_BYTES)
with tempfile.TemporaryDirectory(prefix="ousast-verify-task-") as temporary:
root = Path(temporary)
try:
extract_checkout(archive, root / "input")
task_storage.check(task_storage.WORKSPACE if task_storage.WORKSPACE.is_dir() else root)
bundle = root / "input"
if {p.name for p in bundle.iterdir()} != {"checkout", "products", "spec.json"}:
raise ValueError("expected built checkout, products and spec in one executor archive")
spec = validate_spec(json.loads((bundle / "spec.json").read_bytes()))
demo = root / "demo"
demo.mkdir()
(demo / "demo.json").write_text(json.dumps(spec["demo"]))
result = run_side(Side(bundle / "checkout", products=bundle / "products"), demo, spec["family"], spec["timeout_seconds"])
except task_storage.ScratchLimit:
result = SideRecord("could_not_build", (), 0, "scratch limit", isolation_mode="task-boundary")
upload(urls["RESULT_URL"], json.dumps(asdict(result)).encode())
if __name__ == "__main__":
task_main()