VibeGame-8B / code /src /vibegame /validate.py
khursheed's picture
Release pipeline source, tests, notebook, and licenses
ae36cfb verified
Raw History Blame Contribute Delete
7.79 kB
"""Evidence-producing import and wall-clock runtime validation in an OS sandbox."""
from __future__ import annotations
import hashlib
import json
import math
import os
from pathlib import Path
import platform
import re
import shutil
import signal
import subprocess
import time
from .project import ProjectError, project_sha256, write_project
ERROR_PATTERN = re.compile(r"(?im)(?:\b(?:SCRIPT ERROR|Parse Error|Parser Error|Compile Error|ERROR):|Failed to load script|Failed loading resource|Failed to instantiate|Couldn't load|Segmentation fault|CRASH HANDLER)")
def sandbox_command(command: list[str], project: Path) -> tuple[list[str], str]:
"""Do not read the user's home/Drive or permit network access in the child."""
project = project.resolve()
binary = Path(command[0]).resolve()
home = project / ".sandbox-home"
home.mkdir(exist_ok=True)
(home / "tmp").mkdir(exist_ok=True)
if platform.system() == "Darwin":
raise RuntimeError("macOS validation is disabled: restrictive sandbox crashes the Godot dynamic loader on tested macOS 26.3.1. Use Linux bubblewrap; no local Godot process was launched.")
if platform.system() == "Linux":
bwrap = shutil.which("bwrap")
if not bwrap:
raise RuntimeError("OS sandbox unavailable: install bubblewrap; evaluation fails closed")
wrapped = [bwrap, "--unshare-all", "--die-with-parent", "--new-session", "--cap-drop", "ALL", "--tmpfs", "/proc", "--dev", "/dev", "--tmpfs", "/tmp"]
for item in ("/usr", "/lib", "/lib64", "/bin", "/sbin"):
if Path(item).exists():
wrapped += ["--ro-bind", item, item]
for item in ("/etc/ld.so.cache", "/etc/fonts"):
if Path(item).exists():
wrapped += ["--ro-bind", item, item]
wrapped += ["--ro-bind", str(binary.parent), str(binary.parent), "--bind", str(project), str(project), "--chdir", str(project), "--", *command]
return wrapped, "Linux bubblewrap: unshared namespaces, no network, restricted mounts"
raise RuntimeError("Unsupported OS sandbox; no untrusted code was executed")
def with_resource_limits(command: list[str], *, file_bytes: int = 32 * 1024 * 1024) -> list[str]:
"""External prlimit is safe when validation is called from Python threads."""
limiter = shutil.which("prlimit")
if platform.system() != "Linux" or not limiter:
raise RuntimeError("Linux prlimit is required for bounded generated-code execution")
return [limiter, f"--fsize={file_bytes}:{file_bytes}", "--nofile=256:256", "--core=0:0",
"--as=4294967296:4294967296", "--", *command]
def run_sandboxed(command: list[str], project: Path, log_path: Path, seconds: float, *, expected_running: bool = False) -> dict:
wrapped, sandbox = sandbox_command(command, project)
home = project / ".sandbox-home"
env = {"PATH": "/usr/bin:/bin:/usr/sbin:/sbin", "HOME": str(home), "TMPDIR": str(home / "tmp"),
"XDG_CONFIG_HOME": str(home / "config"), "XDG_DATA_HOME": str(home / "data"),
"XDG_CACHE_HOME": str(home / "cache"), "LANG": "C.UTF-8", "GODOT_SILENCE_ROOT_WARNING": "1"}
start = time.monotonic()
terminated_by_harness = False
with log_path.open("wb") as log:
process = subprocess.Popen(with_resource_limits(wrapped), cwd=project, stdout=log, stderr=subprocess.STDOUT,
env=env, start_new_session=True)
startup_seconds = None
deadline = start + seconds
while process.poll() is None and time.monotonic() < deadline:
if startup_seconds is None:
with log_path.open("rb") as probe:
if b"Godot Engine v4." in probe.read(4096):
startup_seconds = time.monotonic() - start
if expected_running:
deadline = start + startup_seconds + seconds
time.sleep(min(0.1, max(0, deadline - time.monotonic())))
if process.poll() is None:
terminated_by_harness = True
os.killpg(process.pid, signal.SIGTERM)
try:
returncode = process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
returncode = process.wait(timeout=5)
else:
returncode = process.returncode
elapsed = time.monotonic() - start
text = log_path.read_text(errors="replace")
errors = [line for line in text.splitlines() if ERROR_PATTERN.search(line)]
sandbox_failed = bool(re.search(r"(?i)sandbox-exec:|bwrap:|operation not permitted|deny\(.*\)", text))
runtime_started = "Godot Engine v4." in text and not sandbox_failed
return {"command": command, "sandbox": sandbox, "returncode": returncode,
"wall_seconds": round(elapsed, 4), "required_seconds": seconds,
"terminated_by_harness": terminated_by_harness, "sandbox_failed": sandbox_failed,
"runtime_started": runtime_started, "startup_seconds": startup_seconds, "errors": errors[:100], "log": str(log_path),
"log_sha256": hashlib.sha256(log_path.read_bytes()).hexdigest(),
"success": bool(runtime_started and not errors and ((terminated_by_harness and elapsed >= seconds) if expected_running else (not terminated_by_harness and returncode == 0)))}
def validate_project(files: dict, work_dir: Path, godot_binary: str, run_seconds: float = 60) -> dict:
"""Import, then require a complete wall-clock window; preserve all diagnostics.
run_seconds < 60 is allowed only for smoke tests and never qualifies as the
published clean_run_60s metric. Existing evidence directories are not reused.
"""
if not math.isfinite(run_seconds) or run_seconds <= 0:
raise ValueError("run_seconds must be positive and finite")
validation_start = time.monotonic()
work_dir = Path(work_dir).absolute()
for ancestor in (work_dir, *work_dir.parents):
if ancestor.is_symlink():
raise ProjectError(f"Symlink evidence directory rejected: {ancestor}")
work_dir.mkdir(parents=True, exist_ok=True)
evidence = work_dir / "validation.json"
if evidence.exists():
raise FileExistsError(f"Refusing to overwrite evidence: {evidence}")
result = {"compile_success": False, "clean_run": False, "clean_run_60s": False,
"run_seconds": run_seconds, "project_sha256": None, "error": None}
try:
project = write_project(files, work_dir / "project")
result["project_sha256"] = project_sha256(files)
binary = str(Path(godot_binary).expanduser().resolve(strict=True))
result["godot_binary_sha256"] = hashlib.sha256(Path(binary).read_bytes()).hexdigest()
result["preimport_seconds"] = time.monotonic() - validation_start
imported = run_sandboxed([binary, "--headless", "--path", str(project), "--editor", "--import"], project, work_dir / "import.log", 90)
result["import"] = imported
result["compile_success"] = imported["success"]
if imported["success"]:
runtime = run_sandboxed([binary, "--headless", "--path", str(project), "--audio-driver", "Dummy", "--max-fps", "60"], project, work_dir / "runtime.log", run_seconds, expected_running=True)
result["runtime"] = runtime
result["clean_run"] = runtime["success"]
result["clean_run_60s"] = bool(runtime["success"] and run_seconds >= 60)
except (ProjectError, OSError, RuntimeError, subprocess.SubprocessError) as exc:
result["error"] = f"{type(exc).__name__}: {exc}"
with evidence.open("x", encoding="utf-8") as handle:
json.dump(result, handle, indent=2)
handle.write("\n")
return result