rein-aharness/rein_aharness/owner_rehearsal.py
tegwick caaea1e90e
Some checks failed
Governed runtime contract / contract (push) Failing after 10s
Add free bwrap rehearsal to the owner check (REINAH-WP-0008-T01)
metered-once --rehearse runs the admitted runtime's launchers (and claude
--version when the build declares it) inside bwrap: runtime read-only at
the mount path, no network, cleared environment, no credentials, no claim.
Fails on the deleted-build-interpreter trampoline that failed run 6efa9436;
the corrected runtime ee3d0a53 passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 269692@bnt-lap001
Assistant-Session: 8077e68a-0985-4b41-898c-2dc602b0d6be
2026-10-13 23:13:49 +02:00

91 lines
3.1 KiB
Python

"""Free host rehearsal of an admitted owner runtime (REINAH-WP-0008-T01).
The owner check reads only the first line of each launcher. The 2026-09-27
run 6efa9436 passed that and still failed with exit 127, because a launcher
re-executed a deleted build-host interpreter. This rehearsal runs the
launchers the way the sandbox does: inside bwrap, with the runtime read-only
at the mount path, no network, a cleared environment and nothing else
writable. It uses no credentials, provider route, queue or ledger.
"""
from __future__ import annotations
import json
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Sequence
RUNTIME_MOUNT = "/opt/sandboxer/runtime"
# Same read-only system binds as sand-boxer's bwrap extension.
SYSTEM_RO_BINDS = ("/usr", "/bin", "/lib", "/lib64", "/etc/resolv.conf", "/etc/ssl/certs")
PYTHON_LAUNCHERS = ("rein-aharness", "glas-harness")
TIMEOUT_SECONDS = 60
Runner = Callable[[Sequence[str], int], int]
@dataclass(frozen=True)
class Step:
name: str
exit_code: int
@property
def ok(self) -> bool:
return self.exit_code == 0
def bwrap_argv(runtime: Path, command: Sequence[str], *, bwrap_bin: str = "bwrap") -> list[str]:
argv = [
bwrap_bin,
"--unshare-user", "--unshare-pid", "--unshare-ipc",
"--unshare-uts", "--unshare-cgroup", "--unshare-net",
"--tmpfs", "/", "--proc", "/proc", "--dev", "/dev", "--clearenv",
]
for path in SYSTEM_RO_BINDS:
if Path(path).exists():
argv += ["--ro-bind", path, path]
argv += [
"--dir", "/opt", "--dir", "/opt/sandboxer",
"--ro-bind", str(runtime), RUNTIME_MOUNT,
"--dir", "/tmp",
"--setenv", "HOME", "/tmp",
"--setenv", "PATH", f"{RUNTIME_MOUNT}/bin:/usr/bin:/bin",
"--chdir", "/tmp",
]
return argv + list(command)
def _run(argv: Sequence[str], timeout: int) -> int:
try:
return subprocess.run(
list(argv), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=timeout,
).returncode
except subprocess.TimeoutExpired:
return 124
except OSError:
return 127
def _expects_claude(runtime: Path) -> bool:
try:
info = json.loads((runtime / "build-info.json").read_text())
except (OSError, ValueError):
return False
return isinstance(info, dict) and isinstance(info.get("claude"), dict)
def rehearse(runtime: Path, *, run: Runner = _run, bwrap_bin: str | None = None) -> list[Step]:
"""Run each launcher once inside bwrap; return one step per launcher."""
bwrap = bwrap_bin or shutil.which("bwrap")
if bwrap is None:
return [Step("bwrap", 127)]
commands = [(name, [f"{RUNTIME_MOUNT}/bin/{name}", "--help"]) for name in PYTHON_LAUNCHERS]
if _expects_claude(runtime):
commands.append(("claude", [f"{RUNTIME_MOUNT}/bin/claude", "--version"]))
return [
Step(name, run(bwrap_argv(runtime, command, bwrap_bin=bwrap), TIMEOUT_SECONDS))
for name, command in commands
]