#!/usr/bin/env python3 # -*- coding: utf-8 -*- """iso_bootstrap_gate.py — the ISO first-boot harness, run as a gate (R-502, decision 147). Usage: python3 scripts/iso_bootstrap_gate.py Exit 0 harness green AND its planted-broken copy convicted · 1 a harness check failed, or the harness could not see a planted broken banner · 2 NOT CHECKED (no docker, no image, docker itself failed). WHAT IT RUNS. scripts/iso/test/bootstrap-modes.sh — felhom-bootstrap.sh's two modes, the network gate, the console banners against their goldens and the postinst — inside `felhom-iso-assistant:trixie`. Until this gate it ran only by hand, and it was RED for two days before anyone saw (R-586). WHERE IT RUNS. Full runs only — `fast=False` in repo_gates.py, so never in --fast, never in the pre-push hook, never in CI (both call --fast; CI has no docker). Operator ruling 2026-10-06, `09` §3 decision 147. Pinned by scripts/test_iso_bootstrap_gate.py (`test_registered_full_runs_only`). NOT CHECKED IS NEVER A PASS. No docker binary, no image, or a docker error (exit 125-127, a timeout) is exit 2 with the words "not checked". The image is built by hand: docker build -f scripts/iso/Dockerfile.assistant -t felhom-iso-assistant:trixie scripts/iso THE TREE STAYS CLEAN. The harness writes into /work (fakes, logs, /etc/felhom). It never sees the repo: the four inputs are copied to a temp dir, mounted READ-ONLY at /src, and copied to /work INSIDE the container. `--network none`: the harness fakes curl and needs no network. THE BUILT-IN DECOY (the positive control, every run). After a green run the gate runs the harness a second time against a copy of felhom-bootstrap.sh whose print_pairing_banner returns at once — the exact shape that left the household's first screen untested until ISO 1.27.0 (R-496). The harness MUST fail it, naming the banner. If it passes, the instrument is blind and the green run proved nothing: exit 1. A green run is also required to show its own pass line AND at least MIN_OK `ok:` lines — a harness that printed the pass line and checked nothing is not green. """ import os import re import shutil import subprocess import sys import tempfile import uuid ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ISO = os.path.join(ROOT, "scripts", "iso") IMAGE = "felhom-iso-assistant:trixie" TIMEOUT_S = 300 # The harness carried 76 `ok:` checks on 2026-10-06. Far below that, deliberately: this is the floor # of "it ran", not a count to keep in step. MIN_OK = 40 PASS_LINE = "ALL BOOTSTRAP-MODE TESTS PASSED" # The planted decoy: the banner function returns before painting. MUTANT_ANCHOR = "print_pairing_banner() {\n" MUTANT_TEXT = MUTANT_ANCHOR + " return 0 # R-502 planted decoy: the banner never paints\n" # A FAIL line the mutant must produce (ASCII fragment of the harness's own check name). MUTANT_MUST_FAIL = re.compile(r"^\s*FAIL: R-496: banner painted", re.M) INPUTS = [ # (repo path, path under /work) — what the harness reads, per its own header (os.path.join(ISO, "felhom-bootstrap.sh"), "felhom-bootstrap.sh"), (os.path.join(ISO, "pkg", "debian", "postinst"), "postinst"), (os.path.join(ISO, "test", "golden"), "golden"), (os.path.join(ISO, "test", "bootstrap-modes.sh"), os.path.join("test", "bootstrap-modes.sh")), ] def not_checked(why): print("iso-bootstrap: NOT CHECKED — %s" % why) print("iso-bootstrap: INCONCLUSIVE (exit 2) — an unrun harness is never a pass") return 2 def stage(dest, mutate=False): """Copy the harness inputs into dest. Returns None, or a reason the staging failed.""" for src, rel in INPUTS: if not os.path.exists(src): return "harness input missing: %s" % src out = os.path.join(dest, rel) os.makedirs(os.path.dirname(out), exist_ok=True) if os.path.isdir(src): shutil.copytree(src, out) else: shutil.copy2(src, out) if mutate: p = os.path.join(dest, "felhom-bootstrap.sh") with open(p, encoding="utf-8") as f: text = f.read() if text.count(MUTANT_ANCHOR) != 1: return ("the decoy anchor %r occurs %d time(s) in felhom-bootstrap.sh (want 1) — the " "built-in decoy cannot be planted" % (MUTANT_ANCHOR.strip(), text.count(MUTANT_ANCHOR))) with open(p, "w", encoding="utf-8") as f: f.write(text.replace(MUTANT_ANCHOR, MUTANT_TEXT)) return None def run_harness(docker, mutate): """Return (rc, output) of one harness run; rc None means docker itself failed (output says why).""" tmp = tempfile.mkdtemp(prefix="felhom-iso-gate-") name = "felhom-iso-gate-%s" % uuid.uuid4().hex[:12] try: err = stage(tmp, mutate) if err: return None, err cmd = [docker, "run", "--rm", "--name", name, "--network", "none", "-v", "%s:/src:ro" % tmp, IMAGE, "bash", "-c", "mkdir -p /work && cp -a /src/. /work/ && exec bash /work/test/bootstrap-modes.sh"] try: p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=TIMEOUT_S) except subprocess.TimeoutExpired: subprocess.run([docker, "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return None, "the harness did not finish in %d s (container %s removed)" % (TIMEOUT_S, name) out = p.stdout.decode("utf-8", "replace") if p.returncode in (125, 126, 127): return None, "docker run exited %d:\n%s" % (p.returncode, out[-600:]) return p.returncode, out finally: shutil.rmtree(tmp, ignore_errors=True) def main(): docker = shutil.which("docker") if not docker: return not_checked("no docker on PATH") p = subprocess.run([docker, "image", "inspect", IMAGE], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) if p.returncode != 0: return not_checked("image %s is absent or docker is unreachable (%s). Build it with: docker build " "-f scripts/iso/Dockerfile.assistant -t %s scripts/iso" % (IMAGE, p.stderr.decode("utf-8", "replace").strip()[:200], IMAGE)) rc, out = run_harness(docker, mutate=False) if rc is None: return not_checked(out) print(out.rstrip()) oks = len(re.findall(r"^\s*ok: ", out, re.M)) fails = re.findall(r"^\s*FAIL: .*$", out, re.M) if rc != 0 or fails: print("\niso-bootstrap: FAILED — the harness exited %d with %d failing check(s):" % (rc, len(fails))) for f in fails: print(" " + f.strip()) return 1 if PASS_LINE not in out or oks < MIN_OK: print("\niso-bootstrap: FAILED — exit 0 but %s (pass line %s, %d ok lines, want >= %d)" % ("the harness proved nothing", "present" if PASS_LINE in out else "ABSENT", oks, MIN_OK)) return 1 mrc, mout = run_harness(docker, mutate=True) if mrc is None: return not_checked("the built-in decoy could not run: %s" % mout) if mrc == 0 or not MUTANT_MUST_FAIL.search(mout): print("\niso-bootstrap: FAILED — the harness PASSED a bootstrap whose pairing banner never paints " "(exit %d). The instrument is blind; the green run above proves nothing." % mrc) print(mout[-800:]) return 1 print("\niso-bootstrap: built-in decoy convicted (a banner that never paints -> harness exit %d)" % mrc) print("iso-bootstrap gate OK — %d harness checks green in %s" % (oks, IMAGE)) return 0 if __name__ == "__main__": sys.exit(main())