diff --git a/scripts/iso_bootstrap_gate.py b/scripts/iso_bootstrap_gate.py new file mode 100644 index 00000000..ce177977 --- /dev/null +++ b/scripts/iso_bootstrap_gate.py @@ -0,0 +1,157 @@ +#!/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()) diff --git a/scripts/repo_gates.py b/scripts/repo_gates.py index ed9c36d0..33cd7768 100644 --- a/scripts/repo_gates.py +++ b/scripts/repo_gates.py @@ -27,6 +27,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**): 14b. stands where-felhom-stands.yaml: every claim cites a source that resolves, and every 'walked' cites a walk (R-819; it was red and ran in no runner) 15. script-tests every Python test suite under scripts/ (found by a walk), by exit code (R-885) + 15b. iso-bootstrap the ISO first-boot harness in felhom-iso-assistant:trixie, with a built-in + decoy — FULL RUNS ONLY, "not checked" (exit 2) without docker (R-502, decision 147) 16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421) **THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already — @@ -162,6 +164,11 @@ GATES = [ # R-885 — the Python test suites under scripts/ (found by a walk) ran only by hand; a change that broke the # 15 tests of the DooPlex hub-DB scripts, or the 73 of the instructions gate, reached main green. ~20 s. ("script-tests", os.path.join(SCRIPTS, "script_tests_gate.py"), [ROOT], True, False), + # R-502 — the ISO first-boot harness (scripts/iso/test/bootstrap-modes.sh) was run by no gate and was RED + # for two days unseen (R-586). It needs docker and an image, so it is fast=False: FULL RUNS ONLY, never + # the pre-push hook, never CI (operator ruling 2026-10-06, `09` §3 decision 147). Without docker or the + # image it says "not checked" and exits 2 — never a pass. Pinned by test_iso_bootstrap_gate.py. + ("iso-bootstrap", os.path.join(SCRIPTS, "iso_bootstrap_gate.py"), [], False, False), # R-421 — every registered gate across all four repos ships with a decoy test, or is # named in the exemption list with its row. Registered LAST, after every runner was green: # a failing gate refuses every push, which is what instructions_gate learned the hard way. diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index 778c085b..daeb214e 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -53,6 +53,11 @@ COVERS = { "(2026-10-03) an old-shape row under the new header, a near-miss category, an " "old rank tag as Sev, an undefined state word, and a pipe outside backticks"), "decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)", + "iso-bootstrap": ("R-502: SIX decoys + the genuine article in scripts/test_iso_bootstrap_gate.py, run from here, docker-free (a " + "fake docker on a one-directory PATH): a BLIND harness that passes a bootstrap whose " + "pairing banner never paints (the R-496 shape), a failing harness, the pass line with no " + "checks behind it, and no docker / no image / a docker error, each 'not checked' (exit 2); " + "the gate also plants that broken banner itself in the real container on every full run"), "instructions": "R-426: a version literal in a CLAUDE.md's effective text; the same in an HTML comment must pass", "wire-contract": ("R-555: an emitted tag whose name the receiver carries ONLY in a // and a /* */ comment " "must convict (it passed for months as `language` did); the genuine article — the same " @@ -455,6 +460,19 @@ else: _n = _gq.stdout.strip().splitlines()[-1] if _gq.stdout.strip() else "?" print(" ok %-20s %s" % ("guide-quote", _n)) +# ── iso-bootstrap (R-502) ──────────────────────────────────────────────────────────────────────── +# +# Its decoys need a fake docker on a one-directory PATH, so they live in their own file and are RUN from +# here (guide-quote's shape). The suite never reaches the real docker, so this stays CI-safe. +ran += 1 +_ib = subprocess.run([sys.executable, os.path.join("scripts", "test_iso_bootstrap_gate.py")], + cwd=ROOT, capture_output=True, text=True) +if _ib.returncode != 0: + fails.append("iso-bootstrap: its decoy suite FAILED — a decoy did not convict\n%s" + % (_ib.stdout + _ib.stderr)[-800:]) +else: + print(" ok %-20s %s" % ("iso-bootstrap", (_ib.stderr.strip().splitlines() or ["?"])[-1])) + # ── site (R-423) ───────────────────────────────────────────────────────────────────────────────── # A page that exists but is not in PAGES. The content is a perfectly valid page, so only the scope rule can convict. decoy("site/unlisted-page", "site_gates.py", diff --git a/scripts/test_iso_bootstrap_gate.py b/scripts/test_iso_bootstrap_gate.py new file mode 100644 index 00000000..5edc7c60 --- /dev/null +++ b/scripts/test_iso_bootstrap_gate.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Decoys for scripts/iso_bootstrap_gate.py (R-502). DOCKER-FREE — runs on the CI runner (BusyBox + +python3 + git) and on DooPlex alike, and never reaches the real docker. + +HOW. The gate finds docker with shutil.which, so every case runs the REAL gate as a subprocess with +PATH set to ONE temp directory: empty (no docker), or holding a FAKE `docker` written in python with +an absolute shebang (so it needs nothing else on PATH — /usr/bin, where the real docker lives on +DooPlex, is never on it). The fake answers `image inspect` and `run` per FAKE_DOCKER_MODE, and for +`run` reads the STAGED felhom-bootstrap.sh from the `-v :/src:ro` mount, so it can tell the +genuine run from the gate's built-in banner decoy. + +What the fake cannot do is run the harness — that needs root and the image. The harness's power to +see a broken banner is proven by the gate itself, in the real container, on every full run (its +built-in decoy). This file proves the gate's verdicts: that a blind harness, a failing harness, a +harness that checked nothing, and an unrun harness can never read as green. + +Run: python3 scripts/test_iso_bootstrap_gate.py +""" +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +GATE = os.path.join(HERE, "iso_bootstrap_gate.py") +sys.path.insert(0, HERE) +import iso_bootstrap_gate as g # noqa: E402 +import repo_gates # noqa: E402 + +FAKE = r'''#!%(py)s +import os, re, sys +mode = os.environ.get("FAKE_DOCKER_MODE", "genuine") +a = sys.argv[1:] +log = os.environ.get("FAKE_DOCKER_LOG") +if log: + open(log, "a").write(" ".join(a) + "\n") +if a[:2] == ["image", "inspect"]: + sys.exit(1 if mode == "no-image" else 0) +if a[:1] == ["rm"]: + sys.exit(0) +if a[:1] != ["run"]: + sys.exit(3) +if mode == "daemon-error": + print("docker: Error response from daemon: something broke."); sys.exit(125) +src = [x for x in a if x.endswith(":/src:ro")][0][:-len(":/src:ro")] +mutant = "R-502 planted decoy" in open(os.path.join(src, "felhom-bootstrap.sh"), encoding="utf-8").read() +oks = "".join(" ok: check %%d\n" %% i for i in range(60)) +if mode == "fails" or (mutant and mode != "blind"): + print(oks + " FAIL: R-496: banner painted to the console seam\nSOME TESTS FAILED"); sys.exit(1) +if mode == "hollow": + print("ALL BOOTSTRAP-MODE TESTS PASSED"); sys.exit(0) +print(oks + "ALL BOOTSTRAP-MODE TESTS PASSED"); sys.exit(0) +''' + + +def run_gate(mode=None): + """Run the real gate. mode None = no docker on PATH at all.""" + d = tempfile.mkdtemp(prefix="iso-gate-test-") + try: + log = os.path.join(d, "calls.log") + if mode is not None: + p = os.path.join(d, "docker") + with open(p, "w") as f: + f.write(FAKE % {"py": sys.executable}) + os.chmod(p, 0o755) + env = {"PATH": d, "FAKE_DOCKER_MODE": mode or "", "FAKE_DOCKER_LOG": log, + "HOME": d, "PYTHONDONTWRITEBYTECODE": "1"} + r = subprocess.run([sys.executable, GATE], cwd=ROOT, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + calls = "" + if os.path.exists(log): + with open(log) as f: + calls = f.read() + return r.returncode, r.stdout.decode("utf-8", "replace"), calls + finally: + shutil.rmtree(d, ignore_errors=True) + + +class IsoBootstrapGateTest(unittest.TestCase): + + def test_genuine_passes_and_runs_the_decoy_too(self): + rc, out, calls = run_gate("genuine") + self.assertEqual(rc, 0, out) + self.assertIn("built-in decoy convicted", out) + self.assertEqual(calls.count("run "), 2, "want the genuine run AND the decoy run:\n" + calls) + self.assertIn("--network none", calls) + + def test_blind_harness_convicts(self): + # The harness passes a bootstrap whose banner never paints: the R-496 shape. Must be 1. + rc, out, _ = run_gate("blind") + self.assertEqual(rc, 1, out) + self.assertIn("instrument is blind", out) + + def test_failing_harness_convicts(self): + rc, out, calls = run_gate("fails") + self.assertEqual(rc, 1, out) + self.assertIn("FAIL: R-496: banner painted", out) + self.assertEqual(calls.count("run "), 1) + + def test_pass_line_without_checks_convicts(self): + rc, out, _ = run_gate("hollow") + self.assertEqual(rc, 1, out) + self.assertIn("proved nothing", out) + + def test_no_docker_is_not_checked(self): + rc, out, _ = run_gate(None) + self.assertEqual(rc, 2, out) + self.assertIn("NOT CHECKED", out) + + def test_no_image_is_not_checked(self): + rc, out, calls = run_gate("no-image") + self.assertEqual(rc, 2, out) + self.assertIn("NOT CHECKED", out) + self.assertNotIn("run ", calls) + + def test_docker_error_is_not_checked(self): + rc, out, _ = run_gate("daemon-error") + self.assertEqual(rc, 2, out) + + def test_decoy_plants_on_the_real_bootstrap(self): + # The built-in decoy's anchor must exist ONCE in the real script, and the plant must apply. + d = tempfile.mkdtemp() + try: + self.assertIsNone(g.stage(d, mutate=True)) + with open(os.path.join(d, "felhom-bootstrap.sh"), encoding="utf-8") as f: + text = f.read() + self.assertIn("R-502 planted decoy", text) + for _src, rel in g.INPUTS: + self.assertTrue(os.path.exists(os.path.join(d, rel)), rel) + finally: + shutil.rmtree(d, ignore_errors=True) + + def test_registered_full_runs_only(self): + # Decision 147: never in --fast (pre-push hook and CI both run --fast; CI has no docker). + rows = [r for r in repo_gates.GATES if r[0] == "iso-bootstrap"] + self.assertEqual(len(rows), 1, "iso-bootstrap must be registered once in repo_gates.GATES") + self.assertTrue(rows[0][1].endswith("iso_bootstrap_gate.py")) + self.assertIs(rows[0][3], False, "iso-bootstrap must be fast=False (full runs only)") + self.assertIs(rows[0][4], False, "iso-bootstrap must not be exemptible") + + +if __name__ == "__main__": + unittest.main(verbosity=2)