Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decisions 63/64), with complete records
gates / gates (push) Successful in 3s

- family_gate / family_gate_except / min_controller format (README, REUSE); gate family-gate + decoys
- templates/grimmory (v3.5.0, exceptions OPDS/Kobo/KOReader/Komga) + onboarding/grimmory.md
- templates/metube (2026.09.29, no exceptions; ladder .28 -> .29) + onboarding/metube.md; fixture MeTube
- volume-persistence gate: routed port read from the label NAME (R-801, red-proofed); APP_EXERCISE (R-788)
- box_walk: family_cookie; no cached "not gated" while an app has no router

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-02 09:36:49 +02:00
parent de0a9bd29f
commit 96829d0d0f
20 changed files with 1078 additions and 20 deletions
+28 -1
View File
@@ -130,6 +130,14 @@ def gate_cookie(sub):
sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip()
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {sub}.{DOMAIN}", f"{BASE}/"])
loc = _loc(r.stdout)
st = re.match(r"HTTP/\S+ (\d+)", r.stdout or "")
if not loc and (not st or st.group(1) in ("404", "502", "503", "504")):
# the app is not routed at this moment (a restart, an update's stop): NOT the same as "not gated" — a cached ""
# here sent every later call past nothing and read the gate's 401 as the app's (2026-10-02, grim-step)
say(f" gate: {sub} not routed right now ({st.group(1) if st else 'no answer'}) — not cached, asked again next call")
return ""
if "/__family/start" in loc: # v0.287.0: the FAMILY gate — the household's dashboard session vouches, as for
return family_cookie(sub, loc, sess) # the setup gate (decisions 63/64)
if "/__gate/start" not in loc:
GATE[sub] = ""
return "" # not gated (open, or no gate for this app)
@@ -148,6 +156,25 @@ def gate_cookie(sub):
return GATE[sub]
def family_cookie(sub, loc, sess):
"""Pass the FAMILY gate (controller >= 0.287.0) as the household: /__family/start on the dashboard host with the
dashboard session → the app host's /__felhom_gate/fcb → `felhom_famgate`. Never printed."""
import urllib.parse
u = urllib.parse.urlsplit(loc)
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {u.hostname}", "-H", f"Cookie: {sess}",
f"{BASE}{u.path}?{u.query}"])
u = urllib.parse.urlsplit(_loc(r.stdout))
if "/__felhom_gate/fcb" not in u.path:
say(f" family gate: the dashboard did not hand back a callback for {sub}")
GATE[sub] = ""
return ""
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {u.hostname}", f"{BASE}{u.path}?{u.query}"])
m = re.search(r"(?im)^set-cookie:\s*(felhom_famgate=[^;\r\n]+)", r.stdout or "")
GATE[sub] = m.group(1) if m else ""
say(f" family gate: {sub} is family-gated — passed as the household (cookie {'set' if GATE[sub] else 'NOT set'})")
return GATE[sub]
def app_curl(sub, path, *extra, method=None, data=None, timeout=45, _retry=True):
"""A call to the APP's own front door on 9202 — the household's route, not ours. Carries the setup
gate's cookie when the app is gated, merged into a fixture's own Cookie header (never a second one)."""
@@ -172,7 +199,7 @@ def app_curl(sub, path, *extra, method=None, data=None, timeout=45, _retry=True)
r = sh(args, timeout=timeout + 30, inp=data)
body, _, tail = (r.stdout or "").rpartition("\n")
code, _, redir = tail.strip().partition(" ")
if _retry and "/__gate/start" in redir:
if _retry and ("/__gate/start" in redir or "/__family/start" in redir):
GATE.pop(sub, None) # the gate cookie expired or was never taken — log in as the household again
return app_curl(sub, path, *raw, method=method, data=data, timeout=timeout, _retry=False)
return r.returncode, code.strip(), body