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
+545
View File
@@ -0,0 +1,545 @@
"""Key-free repository executor and sequential presigned-object mailbox protocol.
The in-process implementation is a test adapter. Production brains use
ObjectStoreExecutor and an independently dispatched executor task.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import io
import json
import math
import os
import resource
import signal
import stat
import subprocess
import tarfile
import tempfile
import time
import urllib.error
import urllib.request
from collections.abc import Iterator, Mapping
from contextlib import suppress
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Protocol
from urllib.parse import urlsplit
from ..sandbox import SandboxJob
from . import _sandbox, task_storage
MAX_COMMAND_BYTES = 65536
MAX_RESULT_BYTES = 65536
SAFE_ENV = {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}
def clean_environment(_source: Mapping[str, str] | None = None) -> dict[str, str]:
"""Explicit allowlist; even PATH is owned by the executor."""
return {**SAFE_ENV, **task_storage.environment()}
@dataclass(frozen=True)
class Command:
seq: int
name: str
args: dict[str, Any]
timeout_seconds: float = 30
limits: dict[str, Any] = field(default_factory=dict)
def validate(self) -> bytes:
if type(self.seq) is not int or self.seq < 1 or self.seq > 100000:
raise ValueError("invalid sequence")
if self.name not in {"list_files", "read_file", "grep", "run", "stop", "prepare_verification"} or not isinstance(self.args, dict):
raise ValueError("invalid command")
if type(self.timeout_seconds) not in (float, int) or not math.isfinite(self.timeout_seconds) or not 0 < self.timeout_seconds <= 300:
raise ValueError("invalid timeout")
if not isinstance(self.limits, dict) or self.limits.keys() - {
"result_bytes",
"memory_bytes",
"disk_bytes",
"task_wall_seconds",
"max_call_usd",
}:
raise ValueError("invalid limits")
for name in ("result_bytes", "memory_bytes", "disk_bytes"):
if name in self.limits and (type(self.limits[name]) is not int or self.limits[name] < 512):
raise ValueError("invalid resource limit")
data = json.dumps(asdict(self), sort_keys=True, allow_nan=False).encode()
if len(data) > MAX_COMMAND_BYTES:
raise ValueError("command too large")
return data
def _bounded(result: dict[str, Any], maximum: int) -> dict[str, Any]:
result = copy.deepcopy(result)
if len(json.dumps(result).encode()) <= maximum:
return result
result["truncated"] = True
# Preserve structured evidence when possible; otherwise make loss explicit.
while len(json.dumps(result).encode()) > maximum:
fields = [(len(v), k) for k, v in result.items() if isinstance(v, (str, list)) and k not in {"status", "error", "isolation_mode"}]
if not fields or max(fields)[0] == 0:
return {"seq": result["seq"], "error": "result_limit", "truncated": True}
_, key = max(fields)
keep = len(result[key]) // 2
result[key] = result[key][-keep:] if key == "stderr" and keep else result[key][:keep]
return result
class InProcessExecutor:
"""Test adapter; task_boundary is only enabled by the key-free task entrypoint."""
def __init__(self, repo: Path, demo: Path | None = None, *, task_boundary: bool = False) -> None:
self.repo = repo.resolve()
if not self.repo.is_dir():
raise ValueError("checkout unavailable")
self.demo = demo
self.task_boundary = task_boundary
self.seq = 0
self.stopped = False
self.last_digest = ""
self.last_result: dict[str, Any] = {}
self.isolation_mode = "unprobed"
self.prepared_put_url: str | None = None
def submit(
self, name: str, args: dict[str, Any], *, timeout_seconds: float = 30, limits: dict[str, Any] | None = None
) -> dict[str, Any]:
result = self.execute(Command(self.seq + 1, name, args, timeout_seconds, limits or {}))
if result.get("error") in {"ValueError", "KeyError", "TypeError", "FileNotFoundError", "OSError"}:
raise ValueError("repository command rejected")
return result
def execute(self, command: Command) -> dict[str, Any]:
digest = hashlib.sha256(command.validate()).hexdigest()
if command.seq == self.seq:
if digest != self.last_digest:
raise ValueError("retry changed command")
return copy.deepcopy(self.last_result)
if self.stopped:
raise ValueError("executor stopped")
if command.seq != self.seq + 1:
raise ValueError("out of order sequence")
self.deadline = time.monotonic() + command.timeout_seconds
try:
result = self._execute(command)
except task_storage.ScratchLimit:
self.stopped = True
result = {"status": "could_not_build", "reason": "scratch limit", "stopped": True}
except (ValueError, OSError, KeyError, TypeError, TimeoutError) as exc:
result = {"error": type(exc).__name__, "timed_out": isinstance(exc, TimeoutError)}
result = _bounded({"seq": command.seq, **result}, min(MAX_RESULT_BYTES, command.limits.get("result_bytes", MAX_RESULT_BYTES)))
self.seq, self.last_digest, self.last_result = command.seq, digest, copy.deepcopy(result)
return result
def _remaining(self) -> float:
remaining = self.deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("executor deadline")
return remaining
def _path(self, value: str) -> Path:
if not isinstance(value, str) or not value or "\0" in value:
raise ValueError("invalid path")
path = Path(value)
if path.is_absolute() or ".." in path.parts or ".git" in path.parts:
raise ValueError("path escapes checkout")
result = self.repo / path
if any(p.is_symlink() for p in [result, *result.parents] if p.is_relative_to(self.repo)):
raise ValueError("symlink refused")
if not result.resolve().is_relative_to(self.repo):
raise ValueError("path escapes checkout")
return result
def _files(self) -> Iterator[Path]:
for parent, directories, names in os.walk(self.repo, followlinks=False):
self._remaining()
directories[:] = sorted(d for d in directories if d != ".git" and not (Path(parent) / d).is_symlink())
for name in sorted(names):
self._remaining()
path = Path(parent) / name
if stat.S_ISREG(path.lstat().st_mode):
yield path
def _read(self, path: Path) -> bytes:
# O_NOFOLLOW closes the final-component symlink race; repository programs
# are terminated before the next command so there are no concurrent writers.
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
with os.fdopen(fd, "rb") as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError("regular file required")
return stream.read(1024 * 1024)
def _execute(self, command: Command) -> dict[str, Any]:
name, args = command.name, command.args
if self.task_boundary:
task_storage.check(task_storage.WORKSPACE if task_storage.WORKSPACE.is_dir() else self.repo)
if name == "prepare_verification":
return self._prepare_verification(args)
if name == "stop":
self.stopped = True
return {"stopped": True}
if name == "list_files":
files = []
for path in self._files():
files.append(str(path.relative_to(self.repo)))
if len(files) == 1000:
break
return {"files": files}
if name == "read_file":
path = self._path(args["path"])
data = self._read(path)
start = max(1, int(args.get("start", 1)))
lines = min(400, max(1, int(args.get("lines", 400))))
text = "\n".join(data.decode(errors="replace").splitlines()[start - 1 : start - 1 + lines])
return {"text": text[:16384], "input_bytes": len(data), "truncated": len(text) > 16384 or path.stat().st_size > len(data)}
if name == "grep":
pattern = args["pattern"]
if not isinstance(pattern, str) or not pattern:
raise ValueError("nonempty pattern required")
matches = []
for index, path in enumerate(self._files()):
if index >= 10000:
break
for number, line in enumerate(self._read(path).decode(errors="replace").splitlines(), 1):
if pattern in line:
matches.append({"path": str(path.relative_to(self.repo)), "line": number, "text": line[:200]})
if len(matches) == 200:
return {"matches": matches}
return {"matches": matches}
argv = args["command"]
if not isinstance(argv, list) or not argv or not all(isinstance(v, str) and "\0" not in v for v in argv):
raise ValueError("command must be argv")
timeout = min(self._remaining(), max(1, int(args.get("timeout_seconds", 30))))
self.deadline = min(self.deadline, time.monotonic() + timeout)
if self.task_boundary:
self.isolation_mode = "task-boundary"
return self._task_run(argv, timeout, command.limits)
_sandbox.isolation_check(timeout_seconds=self._remaining())
self.isolation_mode = _sandbox.isolation_mode()
with tempfile.TemporaryDirectory(prefix="ousast-executor-") as directory:
result = _sandbox.run(
SandboxJob(
"",
tuple(argv),
self.repo,
{},
max(1, math.ceil(min(timeout, self._remaining()))),
max(1, command.limits.get("memory_bytes", 256 * 1024**2) // 1024**2),
128,
),
scratch=Path(directory),
mounts={"/demo": self.demo} if self.demo else {},
scratch_bytes=command.limits.get("disk_bytes", task_storage.DEFAULT_SCRATCH_BYTES),
wall_timeout_seconds=self._remaining(),
)
return {
"isolation_mode": self.isolation_mode,
"exit_code": result.exit_code,
"stdout": result.stdout[:16384],
"stderr": result.stderr[-16384:],
"timed_out": result.timed_out,
"truncated": len(result.stdout) > 16384 or len(result.stderr) > 16384,
}
def _task_run(self, argv: list[str], timeout: float, limits: dict[str, Any]) -> dict[str, Any]:
def constrain() -> None:
resource.setrlimit(resource.RLIMIT_FSIZE, (limits.get("disk_bytes", task_storage.DEFAULT_SCRATCH_BYTES),) * 2)
resource.setrlimit(resource.RLIMIT_NPROC, (128, 128))
resource.setrlimit(resource.RLIMIT_AS, (limits.get("memory_bytes", 256 * 1024**2),) * 2)
resource.setrlimit(resource.RLIMIT_CPU, (math.ceil(timeout) + 1,) * 2)
timeout = min(timeout, self._remaining())
initial_bytes = self._tree_bytes()
guard_root = task_storage.WORKSPACE if task_storage.WORKSPACE.is_dir() else self.repo
task_storage.check(guard_root)
with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
proc = subprocess.Popen(
argv, cwd=self.repo, env=clean_environment(), stdout=out, stderr=err, start_new_session=True, preexec_fn=constrain
)
timed_out = False
disk_exceeded = False
# Monitor the full disk-backed workspace, including package caches.
# An unnamespaced child can still hardcode RAM-backed paths: this
# guard is not a replacement for the operator's memory limit.
try:
while True:
task_storage.check(guard_root, additional_bytes=os.fstat(out.fileno()).st_size + os.fstat(err.fileno()).st_size)
if self._tree_bytes() - initial_bytes > limits.get("disk_bytes", task_storage.DEFAULT_SCRATCH_BYTES):
disk_exceeded = True
break
try:
proc.wait(timeout=min(0.05, self._remaining()))
task_storage.check(guard_root, additional_bytes=os.fstat(out.fileno()).st_size + os.fstat(err.fileno()).st_size)
if self._tree_bytes() - initial_bytes > limits.get("disk_bytes", task_storage.DEFAULT_SCRATCH_BYTES):
disk_exceeded = True
break
except subprocess.TimeoutExpired:
self._remaining()
except TimeoutError:
timed_out = True
finally:
with suppress(ProcessLookupError):
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
out.seek(0)
err.seek(max(0, os.fstat(err.fileno()).st_size - 16384))
return {
**({"status": "could_not_build", "reason": "scratch limit"} if disk_exceeded else {}),
"isolation_mode": "task-boundary",
"exit_code": proc.returncode,
"timed_out": timed_out,
"disk_limit_exceeded": disk_exceeded,
"aggregate_disk_isolation": "outer-task-quota-required",
"stdout": out.read(16384).decode(errors="replace"),
"stderr": err.read(16384).decode(errors="replace"),
"truncated": os.fstat(out.fileno()).st_size > 16384 or os.fstat(err.fileno()).st_size > 16384,
}
def _prepare_verification(self, spec: dict[str, Any]) -> dict[str, Any]:
"""Build and export through the host-scoped output URL, never a model URL."""
import shutil
from .demo import BUILD_RECIPES, validate_demo
from .verify import _tree
from .verify_task import pack_checkout, validate_spec
if not self.task_boundary or not self.prepared_put_url:
raise ValueError("preparation requires an executor task output")
spec = copy.deepcopy(spec)
validate_demo(spec["demo"])
recipe = spec["demo"]["build"]
spec["demo"]["build"] = {"recipe": "none", "arguments": []}
validate_spec(spec)
with tempfile.TemporaryDirectory(prefix="ousast-built-") as directory:
bundle = Path(directory)
products = bundle / "products"
products.mkdir()
if recipe["recipe"] != "none":
path = self._path(recipe["arguments"][0])
argv = [
arg.replace("/scratch", str(products))
for arg in BUILD_RECIPES[recipe["recipe"]]
if arg not in {"--offline", "--no-index"}
]
argv.append(str(path))
if recipe["recipe"] == "maven":
argv += ["package", "-DskipTests"]
elif recipe["recipe"] == "gradle":
argv += ["assemble"]
built = self._task_run(argv, self._remaining(), {"disk_bytes": task_storage.DEFAULT_SCRATCH_BYTES})
if built.get("reason") == "scratch limit":
raise task_storage.ScratchLimit("scratch limit")
if built["exit_code"] or built["timed_out"]:
return {"status": "could_not_build", "reason": "build recipe failed: " + built["stderr"][-2000:]}
# Validate internal generated links before materializing them as
# regular archive members. No host/external files may be exported.
_tree(self.repo, generated_links=True)
_tree(products, generated_links=True)
shutil.copytree(self.repo, bundle / "checkout", symlinks=False)
materialized = bundle / "materialized"
shutil.copytree(products, materialized, symlinks=False)
shutil.rmtree(products)
materialized.rename(products)
(bundle / "spec.json").write_text(json.dumps(spec))
task_storage.check(task_storage.WORKSPACE if task_storage.WORKSPACE.is_dir() else bundle)
blob = pack_checkout(bundle)
URLTransport().put(self.prepared_put_url, blob, self._remaining())
return {"status": "ok", "archive_bytes": len(blob), "sha256": hashlib.sha256(blob).hexdigest()}
def _tree_bytes(self) -> int:
total = 0
for parent, directories, names in os.walk(self.repo, followlinks=False):
self._remaining()
directories[:] = [name for name in directories if not (Path(parent) / name).is_symlink()]
for name in names:
with suppress(OSError):
info = (Path(parent) / name).lstat()
if stat.S_ISREG(info.st_mode):
total += info.st_size
return total
class Transport(Protocol):
def get(self, url: str, timeout: float) -> bytes | None: ...
def put(self, url: str, data: bytes, timeout: float) -> None: ...
class URLTransport:
"""Only presigned object URLs; redirects are refused to preserve their scope."""
def __init__(self) -> None:
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
return None
self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
def get(self, url: str, timeout: float) -> bytes | None:
try:
with self.opener.open(url, timeout=timeout) as response:
data = bytes(response.read(max(MAX_RESULT_BYTES, MAX_COMMAND_BYTES) + 1))
if len(data) > max(MAX_RESULT_BYTES, MAX_COMMAND_BYTES):
raise ValueError("mailbox object too large")
return data
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise OSError("object read failed") from None
def put(self, url: str, data: bytes, timeout: float) -> None:
with self.opener.open(
urllib.request.Request(url, data=data, method="PUT", headers={"Content-Type": "application/json"}), timeout=timeout
):
pass
class ObjectStoreExecutor:
def __init__(self, command_put_url: str, result_get_url: str, *, transport: Transport | None = None, poll_seconds: float = 0.2) -> None:
for url in (command_put_url, result_get_url):
parts = urlsplit(url)
if parts.scheme != "https" or not parts.hostname or parts.username or parts.password:
raise ValueError("HTTPS presigned URLs required")
self.command_url, self.result_url = command_put_url, result_get_url
self.transport = transport or URLTransport()
self.poll_seconds = poll_seconds
self.seq = 0
self.uncertain = False
self.isolation_mode = "remote-executor"
def submit(
self, name: str, args: dict[str, Any], *, timeout_seconds: float = 30, limits: dict[str, Any] | None = None
) -> dict[str, Any]:
if self.uncertain:
raise RuntimeError("previous command outcome uncertain; discard executor task")
command = Command(self.seq + 1, name, args, timeout_seconds, limits or {})
payload = command.validate()
end = time.monotonic() + timeout_seconds
self.uncertain = True
while time.monotonic() < end:
try:
# A lost PUT acknowledgement is retried with the exact same sequence.
self.transport.put(self.command_url, payload, max(0.001, end - time.monotonic()))
raw = self.transport.get(self.result_url, max(0.001, end - time.monotonic()))
if raw is not None:
if len(raw) > MAX_RESULT_BYTES:
raise ValueError("result too large")
result = json.loads(raw)
if not isinstance(result, dict) or type(result.get("seq")) is not int:
raise ValueError("invalid result envelope")
if result["seq"] > command.seq:
raise ValueError("future result sequence")
if result["seq"] == command.seq:
self.seq, self.uncertain = command.seq, False
self.isolation_mode = result.get("isolation_mode", self.isolation_mode)
return result
except (OSError, TimeoutError):
pass
time.sleep(min(self.poll_seconds, max(0, end - time.monotonic())))
raise TimeoutError("executor result deadline")
def serve(
executor: InProcessExecutor,
command_get_url: str,
result_put_url: str,
*,
transport: Transport | None = None,
deadline_seconds: float = 900,
poll_seconds: float = 0.2,
) -> None:
transport = transport or URLTransport()
end = time.monotonic() + deadline_seconds
while time.monotonic() < end:
try:
raw = transport.get(command_get_url, min(10, max(0.001, end - time.monotonic())))
if raw is not None:
if len(raw) > MAX_COMMAND_BYTES:
raise ValueError("command too large")
command = Command(**json.loads(raw))
if command.seq != executor.seq and command.timeout_seconds > end - time.monotonic():
raise TimeoutError("command exceeds task lifetime")
result = executor.execute(command)
transport.put(result_put_url, json.dumps(result).encode(), min(10, max(0.001, end - time.monotonic())))
if result.get("stopped"):
return
except (OSError, TimeoutError):
pass
time.sleep(min(poll_seconds, max(0, end - time.monotonic())))
raise TimeoutError("executor task deadline")
def unpack_checkout(data: bytes, root: Path) -> None:
"""Bounded regular-file archive only; no links, special files, or metadata."""
if len(data) > 128 * 1024**2:
raise ValueError("checkout archive too large")
root.mkdir(parents=True, exist_ok=False)
with tarfile.open(fileobj=io.BytesIO(data), mode="r:*") as archive:
total = 0
for index, member in enumerate(archive):
path = Path(member.name)
total += member.size
if (
index >= 100000
or total > 256 * 1024**2
or path.is_absolute()
or ".." in path.parts
or ".git" in path.parts
or not (member.isfile() or member.isdir())
):
raise ValueError("unsafe checkout archive")
destination = root / path
if member.isdir():
destination.mkdir(parents=True, exist_ok=True)
else:
destination.parent.mkdir(parents=True, exist_ok=True)
source = archive.extractfile(member)
if source is None:
raise ValueError("archive member is not a regular file")
with source, destination.open("xb") as target:
while chunk := source.read(65536):
target.write(chunk)
destination.chmod(0o755 if member.mode & 0o111 else 0o644)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", type=Path, required=True)
parser.add_argument("--command-get-url")
parser.add_argument("--result-put-url")
parser.add_argument("--deadline", type=float, default=900)
parser.add_argument("--task-boundary", action="store_true")
args = parser.parse_args()
command_url = args.command_get_url or os.environ.get("COMMAND_GET_URL")
result_url = args.result_put_url or os.environ.get("EXECUTOR_RESULT_PUT_URL")
repo_url = os.environ.get("REPO_URL")
prepared_url = os.environ.get("PREPARED_PUT_URL")
# Clearing Python's environment cannot remove initial /proc/self/environ.
# Refuse a misconfigured task before any repository input is opened.
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 SystemExit("executor refuses inherited credentials")
scratch_limit = task_storage.configure()
os.environ.clear()
os.environ.update(clean_environment())
os.environ["OUSAST_SCRATCH_BYTES"] = str(scratch_limit)
if not command_url or not result_url:
raise SystemExit("executor requires presigned mailbox URLs")
if repo_url:
from .verify_task import download
unpack_checkout(download(repo_url, 128 * 1024**2, readiness_budget=min(60, args.deadline)), args.repo)
executor = InProcessExecutor(args.repo, task_boundary=args.task_boundary)
executor.prepared_put_url = prepared_url
serve(executor, command_url, result_url, deadline_seconds=args.deadline)
if __name__ == "__main__":
main()