#!/usr/bin/env python3 # -*- coding: utf-8 -*- """test_gate_decoys.py — can this gate be fooled by a LABEL? (R-421) The controller half of the decoy sweep. Rationale, and the four failure shapes it hunts, are in `felhom.eu/scripts/test_gate_decoys.py` and `documentation/audits/AUDIT-gate-decoys-2026-09-01.md`. TWO HOLES THIS FILE PINS, both measured on 2026-09-01 and both fixed the same day: * **Six gates decided their SCOPE with `os.listdir`**, one directory level. No template subdirectory existed, so every one was green and correct — and would have stayed green the moment anyone added `templates/partials/`, which is an ordinary act. `mojibake` and `docker-v` already used `os.walk` and caught the same planted file, which is the control that proved the cause was the listing and not the decoy. * **`debug-routes` and `app-row-dedup` matched text inside COMMENTS.** A dispatcher case left in a commented-out block counted as a live handler — which is R-400's original defect reached through the one door its own gate could not see. Run from `controller/`: python3 scripts/test_gate_decoys.py Exit 0 all decoys rejected · 1 a decoy passed. """ import io import os import re import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) CTRL = os.path.dirname(HERE) TPL = os.path.join(CTRL, "internal", "web", "templates") SUB = os.path.join(TPL, "partials") # ── WHAT THIS FILE COVERS ──────────────────────────────────────────────────────────────────────── # AST-parsed by felhom.eu/scripts/decoy_coverage_gate.py. See that file for why it is a declaration # and not a grep. COVERS = { "emoji": "an emoji in templates/partials/ (scope was os.listdir)", "native-confirm": "a native confirm() in templates/partials/", "app-row-dedup": "hand-rolled row markup in partials/, AND a commented-out partial call", "template-id": "a JS reference to a missing id, in partials/", "secret-markup": "a secret templated into markup, in partials/", "retrieval-promise": "an unregistered retrieval promise, in partials/; R-325: the shared vocabulary " "absent (INCONCLUSIVE) and an invented stem in it convicting", "mojibake": "CONTROL: already walked; proves the planted file is really reachable", "debug-routes": "a live dispatcher case commented out - the button survives, the handler dies", "golden-notice": "R-410 in the other direction: an empty dir must not count as a bake", "minagent-header": "the word MinAgent in prose / a code span, not the header line (test_minagent_header_gate.py)", "i18n": "an undefined marker key, a pleading English value, a shrinking/growing gap (v0.247.0)", "go-parity": "a Go-side key REWORDED, a key citing text no base literal has, and a converted " "key left out of the map (v0.252.0, R-557); a call that lost an argument, and a key glued " "to more text with + (R-576)", "gofmt": "R-454: a planted unformatted .go file in internal/ is convicted; the clean tree passes", "offbox-rename": "R-425: NAS branding in a NEW backups*.html, and in a bundle value an offbox Go file " "names; control: the same token in the network-storage feature's copy is accepted", "docker-v": "R-426: an unallowlisted `-v /etc:/x` in a NEW .go file two directories down " "(internal/ and cmd/) convicts; controls: the clean tree, the same line in a _test.go", "reuse-refs": "R-426: a cited .go and a cited .md path that do not exist, planted in THIS repo's " "REUSE.md - vs the real file", "instructions": "R-426: a component version literal in THIS repo's CLAUDE.md effective text - vs the " "same sentence inside an HTML comment", "observations": "R-426: R-419 in THIS repo's REPORT.md - an Observations note SAYING it carries no " "marker - vs the two genuine markers", } fails = [] ran = 0 def gate(script): p = subprocess.run([sys.executable, os.path.join("scripts", script)], cwd=CTRL, capture_output=True, text=True) return p.returncode, p.stdout + p.stderr def in_subdir(name, script, content): """Plant a template one directory down and assert the gate still sees it.""" global ran ran += 1 made = not os.path.isdir(SUB) if made: os.makedirs(SUB) f = os.path.join(SUB, "decoy.html") io.open(f, "w", encoding="utf-8").write(content) try: rc, out = gate(script) finally: os.remove(f) if made and os.path.isdir(SUB) and not os.listdir(SUB): os.rmdir(SUB) if rc == 0: fails.append("%s: a planted template in templates/partials/ PASSED — the gate's scope is a " "directory listing, not the set of templates (R-421)\n%s" % (name, out[-400:])) else: print(" ok %-20s sees templates at any depth" % name) def swapped(name, script, path, transform, expect="convict"): global ran ran += 1 b = io.open(path, encoding="utf-8").read() try: io.open(path, "w", encoding="utf-8").write(transform(b)) rc, out = gate(script) finally: io.open(path, "w", encoding="utf-8").write(b) if (rc != 0) != (expect == "convict"): fails.append("%s: rc=%d, expected %s\n%s" % (name, rc, expect, out[-400:])) else: print(" ok %-20s %s" % (name, "decoy rejected" if expect == "convict" else "genuine accepted")) print("decoys — felhom-controller") # --- SCOPE: the six listdir gates, each with content that actually triggers it ----------------- in_subdir("emoji", "emoji_gate.py", u"

Kesz \U0001F600

\n") in_subdir("native-confirm", "native_confirm_gate.py", u"\n") in_subdir("app-row-dedup", "app_row_dedup_gate.py", u'
hand-rolled
\n') in_subdir("template-id", "template_id_gate.py", u'
\n\n') in_subdir("secret-markup", "secret_in_markup_gate.py", u'\n') in_subdir("retrieval-promise", "retrieval_promise_gate.py", u"

A jelszavat barmikor visszaallithatja innen.

\n" u"

Bovebben: visszaállítható a kóddal.

\n") # --- SCOPE OF THE ID GATE'S INCLUDE-FOLLOWING (v0.315.0) --------------------------------------- # The gate now unions in the ids of every template a page {{template}}s, because the app page's form # fields live in one partial rendered by two pages. The hazard that buys is a VACUOUS check: if the # include walk reached every file, every id would resolve everywhere and the gate would convict # nothing. So: plant, in deploy.html, a reference to an id defined only in app_data.html — a file # deploy.html does not include. The gate must still convict. swapped("template-id scope", "template_id_gate.py", os.path.join(TPL, "deploy.html"), lambda b: b.replace("