"""vwstep.py — R-890: vaultwarden's step on scratch 9202 (drill catalog), ONE process, through the product. 1 install vaultwarden fresh at the live pin (this run installs it — the admin seed refuses otherwise); 2 seed through the household's door, the invite through the admin page INSIDE the box (FELHOM_BOX_ADMIN_SEED=1, upgrade_fixtures_box.box_admin_seed_allowed); read it back (C1); 3 a DRILL-only commit moves the image and adds a ladder entry; sync, rescan; 4 the product's guarded Update; the seed read back; box verdict JSON; 5 remove through the product. Evidence: ../box/vaultwarden/step.txt + box-verdict-vaultwarden.json. The live entry is written ONLY by `upgrade-test.py --write-ladder` from both verdicts.""" import json, os, re, subprocess, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import box_walk as w import upgrade_fixtures_box as fixtures APP, SUB, SVC = "vaultwarden", "vault", "vaultwarden" to = sys.argv[1] HERE = os.path.dirname(os.path.abspath(__file__)) EVD = os.path.join(HERE, "..", "box", APP); os.makedirs(EVD, exist_ok=True) log = open(f"{EVD}/step.txt", "a", buffering=1) D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill" def say(*a): w.say(*a); log.write(" ".join(map(str, a)) + "\n") fx = fixtures.FIXTURES[APP] w.login() if w.stack(APP).get("deployed"): say("vaultwarden already installed on 9202 — removing it first (scratch box)") w.remove(APP) w.sync_rescan() if not w.deploy(APP, SUB): sys.exit(say("RESULT the install did not complete") or 1) tok = fx.seed(w, SUB, say) if tok is None or not fx.verify(w, SUB, tok, say): say(f"RESULT C1 failed: {getattr(fx, 'tried', '')}") w.remove(APP); sys.exit(1) say("C1 seed reads back BEFORE: True") before = (w.stack(APP).get("app_config") or {}).get("pinned_images") say(f"before: pinned={before}") subprocess.run(["git", "-C", D, "pull", "-q", "--rebase", "origin", "main"], check=True) comp, fy = f"{D}/templates/{APP}/docker-compose.yml", f"{D}/templates/{APP}/.felhom.yml" s = open(comp).read() frm = re.search(r"^\s+image:\s*(\S+)", s, re.M).group(1) open(comp, "w").write(s.replace("image: " + frm, "image: " + to, 1)) entry = {"from": {SVC: frm}, "to": {SVC: to}, "verdict": "proven", "tested_at": "DRILL", "harness_version": 5, "evidence": "DRILL (box proof in progress)", "marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}} f = open(fy).read() f = (f.rstrip("\n") + "\n - " + json.dumps(entry) + "\n") if "update_ladder:" in f else (f.rstrip("\n") + "\nupdate_ladder:\n - " + json.dumps(entry) + "\n") open(fy, "w").write(f) subprocess.run(["git", "-C", D, "commit", "-q", "-am", f"DRILL {APP}: {frm} -> {to} (box proof, R-890)"], check=True) subprocess.run(["git", "-C", D, "push", "-q", "origin", "main"], check=True, capture_output=True) say("drill:", subprocess.run(["git", "-C", D, "log", "--oneline", "-1"], capture_output=True, text=True).stdout.strip()) w.sync_rescan(APP, to) say(f"badge before: {w.badges(APP)}") since = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip() res = w.press_update(APP, poll=1, cap_s=1800) for p in res.get("phases", []): log.write(f" phase +{p['t']}s {p['phase']} | err={p['error']}\n") time.sleep(10) read = fx.verify(w, SUB, tok, say) lines = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update {APP}' | grep -v DEBUG | cut -c1-400") log.write(lines + "\n") st = w.stack(APP); after = (st.get("app_config") or {}).get("pinned_images") verdict = {"app": APP, "venue": "box 9202 (drill catalog), the product's guarded Update; seeded through the admin invite inside the box (R-890)", "from": before, "to": after, "verdict": "proven" if (res.get("final_phase") == "done" and read and (after or {}).get(SVC) == to) else "failed", "seed_read_before": True, "seed_read_after": read, "healthy_after": st.get("state") == "running", "duration_s": res.get("duration_s"), "final_phase": res.get("final_phase"), "measured_at": since, "evidence": "felhom.eu/documentation/audits/r890-instructions-2026-10-06/box/vaultwarden/step.txt"} json.dump(verdict, open(f"{EVD}/box-verdict-{APP}.json", "w"), indent=2) say(f"badge after: {w.badges(APP)}") say(f"RESULT final_phase={res.get('final_phase')} after={after} seed_after={read} verdict={verdict['verdict']} ({res.get('duration_s')} s)") say(f"remove -> {w.remove(APP)}")