box_walk: a target table (9202, 9201, tester-1 via a jump through the HP box); the Tester 1 guest is a test box for the admin seed (R-892, decision 158)
gates / gates (push) Successful in 5s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 19:04:57 +02:00
parent a5a5b51c77
commit d63ea3591e
4 changed files with 101 additions and 12 deletions
+9
View File
@@ -1,3 +1,12 @@
## 2026-10-06 (night) — the box walk's target table; the Tester 1 box is a test box (R-892)
**What runs on a box changed:** nothing. Test tools only.
- `box_walk.py` `TARGETS`: one row per box (name for the guards, ssh host + optional jump, guest, dashboard base and domain): `9202` (default), `9201`, and `tester-1` — VM 341 on the HP box (node `felhom`, 192.168.0.154, reached with `-J demo-hp`), guest 9201 at 192.168.0.101 for `felhom.enkicsifelhom.hu`. `TARGET=` selects; the old `GUEST=` still works. Nothing edits DooPlex's `~/.ssh/config`.
- `BOX_ADMIN_SEED_GUESTS` adds `("tester-1", "9201")` (`09` §3 decisions 149, 158); demo-hp's 9201 stays refused.
- Tests: BoxWalkTargets (5, red-proved: without the jump the Tester 1 test fails), one more BoxAdminSeedGuard case.
- **Not yet usable on the Tester 1 box:** DooPlex's key is not authorized there (`Permission denied (publickey,password)`), and fetching its vaulted password was refused by the session's permission check.
## 2026-10-06 (evening) — wishlist's own sign-up switch closes after the setup and reopens for the family window (R-717)
**What runs on a box changed:** wishlist's `.felhom.yml` gains `after_setup` (`command` closes, `open_command` opens — Node's own sqlite module writes `system_config.enableSignup` in group `global`, the value read back before the marker prints) and `min_controller: "0.301.0"` (the controller that knows `open_command`). Pushed AFTER controller v0.301.0 reached demo-hp, demo-felhom and the Tester 1 box. A catalog change does not touch an installed wishlist (the 2026-09-29 Part 0 rule); no box reports wishlist today. Proven live on 9202 with the 0.301.0 test image: after the setup the switch read closed and a stranger straight at the app got 401 „invite only"; in the 15-minute window it read open and a family member signed up (users 1 → 2); at the window's end the close ran again 10 s later and a stranger got 401, users stayed 2 (`felhom.eu/documentation/audits/design-build-2026-10-06/E/live.txt`). Opengist cannot use it: its container has no sqlite tool and no script runtime, and its CLI has no settings command (R-717 stays open for opengist).
+35 -6
View File
@@ -28,12 +28,41 @@ from datetime import datetime, timezone
SC = os.environ.get('SC', os.path.expanduser('~/.felhom-retest'))
EV = os.environ.get('EV', os.path.join(SC, 'evidence'))
DRILL = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
# GUEST=9201 selects demo-hp's hub-enabled guest (the mail proof); default 9202, the scratch guest.
GUEST = os.environ.get("GUEST", "9202")
BASE = os.environ.get("BASE") or {"9202": "https://192.168.0.114", "9201": "https://192.168.0.155"}[GUEST]
DOMAIN = os.environ.get("DOMAIN", "enkisfelhom.hu")
# THE TARGETS (R-892, 2026-10-06): one row per box the walk may drive. `hp` is the box's NAME for the guards
# (upgrade_fixtures_box.BOX_ADMIN_SEED_GUESTS keys on (hp, guest)); `ssh` is how its Proxmox host is reached, with an
# optional `jump` (never an edit of DooPlex's ~/.ssh/config); `guest` the customer LXC; `base` and `domain` its dashboard.
# TARGET=9202 (default) scratch guest 9202 on demo-hp
# TARGET=9201 demo-hp's hub-enabled guest (the mail proof) — household-shaped, never seeded through an admin
# TARGET=tester-1 the Tester 1 box: VM 341 on the HP box (`09` §3 decision 158), its node `felhom` at
# 192.168.0.154, guest 9201 at 192.168.0.101 for felhom.enkicsifelhom.hu (identity matched
# 2026-10-06: the agent's report `host.node=felhom` = the VM's certificate; the guest answers
# that domain, the other one 404 — `felhom.eu/documentation/audits/readback-2026-10-07/`)
# GUEST=<vmid> still selects a demo-hp row (the old switch), unless TARGET is set.
TARGETS = {
"9202": {"hp": "demo-hp", "ssh": "demo-hp", "jump": None, "guest": "9202",
"base": "https://192.168.0.114", "domain": "enkisfelhom.hu"},
"9201": {"hp": "demo-hp", "ssh": "demo-hp", "jump": None, "guest": "9201",
"base": "https://192.168.0.155", "domain": "enkisfelhom.hu"},
"tester-1": {"hp": "tester-1", "ssh": "root@192.168.0.154", "jump": "demo-hp", "guest": "9201",
"base": "https://192.168.0.101", "domain": "enkicsifelhom.hu"},
}
TARGET = os.environ.get("TARGET") or os.environ.get("GUEST", "9202")
if TARGET not in TARGETS:
raise SystemExit(f"box_walk: unknown TARGET {TARGET!r} — one of {sorted(TARGETS)}")
_T = TARGETS[TARGET]
GUEST = _T["guest"]
BASE = os.environ.get("BASE") or _T["base"]
DOMAIN = os.environ.get("DOMAIN") or _T["domain"]
HOSTHDR = f"Host: felhom.{DOMAIN}"
HP = "demo-hp"
HP = _T["hp"]
def ssh_args():
"""The ssh argv prefix that reaches the target's Proxmox host (a jump when the row names one)."""
a = ["ssh", "-o", "ConnectTimeout=20", "-o", "StrictHostKeyChecking=accept-new"]
if _T["jump"]:
a += ["-J", _T["jump"]]
return a + [_T["ssh"]]
LOG = []
@@ -58,7 +87,7 @@ def guest(script, timeout=600):
# two concurrent walks (memory: guest-helper-shares-one-tmp-file).
import secrets as _s
t = f"/tmp/w{GUEST}-{os.getpid()}-{_s.token_hex(4)}.sh"
r = sh(["ssh", "-o", "ConnectTimeout=20", "-o", "StrictHostKeyChecking=accept-new", HP,
r = sh(ssh_args() + [
f"export LC_ALL=C; cat > {t}; pct push {GUEST} {t} {t} >/dev/null 2>&1; "
f"pct exec {GUEST} -- bash {t}; pct exec {GUEST} -- rm -f {t}; rm -f {t}"],
timeout=timeout, inp=script)
+51
View File
@@ -346,6 +346,12 @@ class BoxAdminSeedGuard(unittest.TestCase):
self.assertIsNone(got)
self.assertIn("test-box admin invite", fx_.tried)
def test_the_tester_1_box_is_a_test_box_and_demo_hp_9201_is_not(self):
self.assertEqual(self.allowed(FakeBoxWalk(hp="tester-1", guest="9201"), self.ENV), (True, ""))
ok, why = self.allowed(FakeBoxWalk(hp="demo-hp", guest="9201"), self.ENV)
self.assertFalse(ok)
self.assertIn("not a test box", why)
def test_only_a_drill_address_is_invited(self):
with self.assertRaises(ValueError):
fxbox.box_admin_invite(FakeBoxWalk(), 'x"}; rm -rf /; {"@gate.invalid')
@@ -390,6 +396,51 @@ class DefinitionEdge(unittest.TestCase):
ut.add_definition_edge("app1", "same")
class BoxWalkTargets(unittest.TestCase):
"""R-892: box_walk's target table. Each test imports box_walk afresh under its own TARGET. RED-PROOF (REPORT): drop
the `-J` from ssh_args — test_the_tester_1_box_is_reached_through_the_hp_box fails."""
def load(self, env):
import importlib
with mock.patch.dict(os.environ, env, clear=False):
for k in ("TARGET", "GUEST", "BASE", "DOMAIN"):
if k not in env:
os.environ.pop(k, None)
sys.modules.pop("box_walk", None)
return importlib.import_module("box_walk")
def test_every_row_is_complete(self):
bw = self.load({})
for name, row in bw.TARGETS.items():
self.assertEqual(set(row), {"hp", "ssh", "jump", "guest", "base", "domain"}, name)
self.assertTrue(row["base"].startswith("https://"), name)
def test_the_default_stays_scratch_9202(self):
bw = self.load({})
self.assertEqual((bw.HP, bw.GUEST, bw.BASE), ("demo-hp", "9202", "https://192.168.0.114"))
self.assertEqual(bw.ssh_args()[-1], "demo-hp")
self.assertNotIn("-J", bw.ssh_args())
def test_the_old_guest_switch_still_selects_demo_hp_9201(self):
bw = self.load({"GUEST": "9201"})
self.assertEqual((bw.HP, bw.GUEST, bw.BASE), ("demo-hp", "9201", "https://192.168.0.155"))
def test_the_tester_1_box_is_reached_through_the_hp_box(self):
bw = self.load({"TARGET": "tester-1"})
self.assertEqual((bw.HP, bw.GUEST, bw.DOMAIN), ("tester-1", "9201", "enkicsifelhom.hu"))
a = bw.ssh_args()
self.assertEqual(a[a.index("-J") + 1], "demo-hp")
self.assertEqual(a[-1], "root@192.168.0.154")
self.assertIn(("tester-1", "9201"), fxbox.BOX_ADMIN_SEED_GUESTS)
def test_an_unknown_target_is_refused(self):
with self.assertRaises(SystemExit):
self.load({"TARGET": "ep0"})
def tearDown(self):
sys.modules.pop("box_walk", None)
class SecretHygiene(unittest.TestCase):
"""The run's secrets: .env 0600 and shredded, every evidence file redacted. RED-PROOF (REPORT): make redact_tree
return [] without rewriting — test_evidence_files_are_redacted fails."""
+6 -6
View File
@@ -306,11 +306,10 @@ def bench_admin_seed_allowed(w):
# --- R-890 (`09` §3 decision 149): the TEST BOXES may seed vaultwarden through its admin route too ---------------------
BOX_ADMIN_SEED_ENV = "FELHOM_BOX_ADMIN_SEED" # the run's explicit opt-in on a box walk
# The test boxes the walk can reach, as (Proxmox host, guest). Scratch 9202 only: box_walk.py drives guests on demo-hp
# alone (its HP), and 9201 there is demo-hp's household-shaped guest — a demo box's household apps are never seeded
# through their admin (decision 149). The Tester 1 box is allowed by the ruling but the walk has no route to it; add
# it here the day the walk can run there.
BOX_ADMIN_SEED_GUESTS = {("demo-hp", "9202")}
# The test boxes, as (box_walk target's `hp` name, guest): scratch 9202 on demo-hp and the Tester 1 box's guest 9201
# (VM 341 on the HP box, `09` §3 decisions 149 and 158 — disposable). demo-hp's 9201 is household-shaped and is NEVER
# here: a demo box's household apps are never seeded through their admin.
BOX_ADMIN_SEED_GUESTS = {("demo-hp", "9202"), ("tester-1", "9201")}
DRILL_CATALOG_MARK = "app-catalog-drill" # a test run points the box at the drill catalog (`09` §6.5)
@@ -350,7 +349,8 @@ T=$(docker exec "$c" printenv ADMIN_TOKEN 2>/dev/null)
H=$(mktemp); chmod 600 "$H"
code=$(printf %s "$T" | curl -s -o /dev/null -D "$H" -w '%{http_code}' --max-time 30 --data-urlencode token@- "http://$ip:80/admin")
T=
ck=$(grep -i '^set-cookie: *VW_ADMIN=' "$H" | head -1 | sed -e 's/^[Ss]et-[Cc]ookie: *//' -e 's/;.*//' | tr -d ' ')
ck=$(grep -i '^set-cookie: *VW_ADMIN=' "$H" | head -1 | sed -e 's/^[Ss]et-[Cc]ookie: *//' -e 's/;.*//' | tr -d '
')
if [ -z "$ck" ]; then shred -u "$H"; echo "RESULT signin=$code session=no"; exit 0; fi
printf 'Cookie: %s
' "$ck" > "$H"; ck=