#!/usr/bin/env python3 """ceremony.py — one escrow ceremony on Tester 1 through the wizard's own endpoints (hub 0.175.0 check, 2026-10-29). The recovery code lives in this process's memory only: never printed, never written, dropped at the end. The dashboard password is read from the 0600 file and handed to curl on stdin (never on a command line).""" import json, os, subprocess, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") SCR = "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad" os.environ.setdefault("SC", f"{SCR}/rt-tester1") os.environ.setdefault("TARGET", "tester-1") import box_walk as w w.login() sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip() csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip() BASE_ARGS = ["curl", "-sk", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}"] def call(method, path, form_stdin=None, body=None): args = BASE_ARGS + ["-X", method, "-w", "\n%{http_code}"] inp = None if form_stdin is not None: args += ["--data-urlencode", "password@-"] inp = form_stdin if body is not None: args += ["-H", "Content-Type: application/json", "--data-binary", "@-"] inp = json.dumps(body) r = subprocess.run(args + [f"{w.BASE}{path}"], input=inp, capture_output=True, text=True, timeout=120) out, _, code = r.stdout.rpartition("\n") try: return code, json.loads(out) except Exception: return code, {"raw": out[:200]} def safe(d): """A copy with the code removed — the only form that is printed.""" d = json.loads(json.dumps(d)) if isinstance(d.get("data"), dict): d["data"].pop("recovery_code", None) return d t0 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) code, d = call("GET", "/api/escrow/preflight") w.say(f"preflight → {code} {json.dumps(d)[:600]}") pw = open(f"{w.SC}/.ctlpw").read().strip() code, d = call("POST", "/api/escrow/start", form_stdin=pw) pw = None w.say(f"start → {code} {json.dumps(safe(d))[:300]}") if code != "200": sys.exit(1) for _ in range(120): time.sleep(3) code, d = call("GET", "/api/escrow/status") ph = (d.get("data") or {}).get("phase") if ph not in ("running", "queued", "starting", None) or (d.get("data") or {}).get("claimable"): break w.say(f"status → {code} {json.dumps(safe(d))[:400]}") code, d = call("POST", "/api/escrow/claim") rc = (d.get("data") or {}).get("recovery_code") or "" pos = (d.get("data") or {}).get("verify_positions") or [] w.say(f"claim → {code}; code received: {bool(rc)} ({len(rc.split('-'))} words); confirm_required={(d.get('data') or {}).get('confirm_required')} positions={pos}") if rc and pos: words = rc.split("-") picked = [words[pos[0] - 1], words[pos[1] - 1]] rc, words = None, None code, d = call("POST", "/api/escrow/confirm", body={"words": picked}) picked = None w.say(f"confirm → {code} {json.dumps(safe(d))[:400]}") rc = None for _ in range(40): time.sleep(15) st = w.guest("docker logs --since " + t0 + " felhom-controller 2>&1 | grep -iE 'escrow' | grep -viE 'recovery_code' | tail -12") if "escrowed" in st or "auto-confirm" in st.lower(): break w.say("controller escrow lines:\n" + st) open(f"{SCR}/d63/ev/ceremony.log", "a").write("\n".join(w.LOG) + "\n")