27d1165962
gates / gates (push) Successful in 17s
Site one. app_info.html rendered {{.InitialCreds.Password}} into a hidden span —
a REAL per-install credential, read live out of the running container, in the
response body of every render. The page now carries the non-secret half plus a
boolean; the value comes from POST /apps/<slug>/initial-credentials/reveal, which
RE-READS the container rather than serving a cached copy (caching it in the
handler would put it back in the body one layer in). no-store, CSRF-covered,
logged as an act. Both buttons go through it. A reveal that cannot read the value
SAYS SO rather than returning an empty string that renders as a blank password.
Site two, established before changing. The hidden input is NOT the defect and was
left alone: it fires only pre-deploy, and README §318 documents why the value must
round-trip — the customer notes the generated secrets down and submitting them
back is what makes the saved value the same one they saw. The defect was the
neighbouring READONLY input, which on an ALREADY-DEPLOYED app rendered the secret
into a page with nothing to submit. Fixed by POST /stacks/<name>/auto-field/reveal,
authorised by requiring a type:secret auto-field of that stack. Both directions
pinned.
The premise that this contradicted a repo rule does not hold: the rule is
CONTEXT.md:2070 'Password fields require explicit input — prevents accidental
empty-password deployments', about EMPTINESS. No line in the repo says 'no silent
auto-fill'.
The gate. scripts/secret_in_markup_gate.py, registered in controller_gates.py,
convicts any template expression that names a secret unless allowlisted with a
reason. Its limits are MEASURED and in its docstring: it catches a launder through
a local variable (the assignment names the secret) but is blind to a secret
arriving under a neutral page-data key — verified both ways. That is the shape of
site two, which this gate would NOT have caught. The runtime body assertion covers
all shapes but only 4 of 27 page templates; the other 23 are R-255, filed rather
than glossed. Two nets, different holes, both named.
Correction to v0.207.0's report: HTML comments do NOT ship in the response body
here — html/template strips them, text/template does not. Measured. A red-proof
planting a secret in a comment therefore correctly does not fail.
140 lines
7.0 KiB
Python
140 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""controller_gates.py — THE entry point for this repo's gates. Run from `controller/`:
|
|
|
|
python3 scripts/controller_gates.py # every gate
|
|
python3 scripts/controller_gates.py --fast # only gates that touch no network and no
|
|
# container runtime (what .githooks/pre-push runs)
|
|
|
|
Gates, in order (all must pass; **non-zero exit on any failure**):
|
|
|
|
1. template-id every template id/handle referenced by the dashboard resolves
|
|
2. emoji no emoji in the Hungarian UI (design-system v2)
|
|
3. native-confirm no native confirm()/prompt() — they freeze browser automation
|
|
4. offbox-rename the persisted `offbox` key is never re-guessed as `offbox_target`
|
|
5. app-row-dedup no duplicated app-row markup in the dashboard templates
|
|
6. mojibake no double-encoded UTF-8 in Hungarian copy
|
|
7. docker-v every `docker … -v` mount is a named volume or a proven host path
|
|
8. reuse-refs every path cited by this repo's REUSE.md still resolves
|
|
|
|
WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)).
|
|
|
|
A census of all thirteen gate scripts across the four felhom repos found one clean correlation:
|
|
**every check a CLAUDE.md tells a person to run was passing, and two of the four nobody is told
|
|
to run were failing.** Of the seven gates above, this repo's CLAUDE.md named exactly two; four
|
|
were reachable only through a line in REUSE.md, and `docker_run_volume_path_gate.py` — RED at the
|
|
time of the census — through one line in REUSE.md and nothing else. The fix is not more gates, it
|
|
is one place to run them from. `app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical
|
|
shape (R-161) and this copies it deliberately rather than inventing a second one.
|
|
|
|
THE SHARED CHECKER. `reuse_refs_check.py` lives in ONE place — `felhom.eu/scripts/` — and is
|
|
invoked here across the workspace at `<repo-root>/../felhom.eu/scripts/`. It is deliberately NOT
|
|
copied into this repo: duplicating it would recreate exactly the drift it exists to detect. If the
|
|
sibling clone is absent the gate FAILS and prints the path it tried — fail-closed, because a
|
|
runner that quietly skips a gate is the inert-seam failure this project has shipped four times.
|
|
|
|
EXIT CODES. Each gate returns 0 clean / 1 convicted / 2 inconclusive. This runner exits non-zero
|
|
if any gate is non-zero, and reports 2 distinctly as INCONCLUSIVE — an undetermined result is
|
|
never a pass, but it is not a conviction either.
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
SCRIPTS = os.path.dirname(os.path.abspath(__file__))
|
|
CTRL = os.path.dirname(SCRIPTS) # <repo>/controller — every gate's cwd
|
|
REPO = os.path.dirname(CTRL) # <repo> — the root REUSE.md lives here
|
|
SHARED_REUSE = os.path.join(os.path.dirname(REPO), "felhom.eu", "scripts", "reuse_refs_check.py")
|
|
SHARED_INSTRUCTIONS = os.path.join(
|
|
os.path.dirname(REPO), "felhom.eu", "scripts", "instructions_gate.py")
|
|
|
|
# (label, absolute script path, args, fast)
|
|
GATES = [
|
|
("template-id", os.path.join(SCRIPTS, "template_id_gate.py"), [], True),
|
|
("emoji", os.path.join(SCRIPTS, "emoji_gate.py"), [], True),
|
|
("native-confirm", os.path.join(SCRIPTS, "native_confirm_gate.py"), [], True),
|
|
("offbox-rename", os.path.join(SCRIPTS, "offbox_rename_gate.py"), [], True),
|
|
("app-row-dedup", os.path.join(SCRIPTS, "app_row_dedup_gate.py"), [], True),
|
|
("mojibake", os.path.join(SCRIPTS, "mojibake_gate.py"), [], True),
|
|
("docker-v", os.path.join(SCRIPTS, "docker_run_volume_path_gate.py"), [], True),
|
|
("secret-markup", os.path.join(SCRIPTS, "secret_in_markup_gate.py"), [], True),
|
|
("reuse-refs", SHARED_REUSE, [REPO], True),
|
|
("instructions", SHARED_INSTRUCTIONS, [REPO], True),
|
|
]
|
|
|
|
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
|
|
|
|
|
def hooks_armed_note(root):
|
|
"""Print a WARNING (never a failure) when this clone's pre-push hook is not switched on.
|
|
|
|
core.hooksPath is local config and a clone does not carry it, so an unarmed clone is silent
|
|
by construction — this is the only place it becomes visible.
|
|
"""
|
|
try:
|
|
val = subprocess.check_output(["git", "config", "--get", "core.hooksPath"],
|
|
cwd=root, stderr=subprocess.DEVNULL).decode().strip()
|
|
except Exception:
|
|
val = ""
|
|
norm = val.replace("\\", "/").rstrip("/")
|
|
if norm == ".githooks" or norm.endswith("/.githooks"):
|
|
return
|
|
print("WARNING: this clone is UNARMED — core.hooksPath is %s, so the pre-push hook will not\n"
|
|
" run here. Switch it on once with: git config core.hooksPath .githooks"
|
|
% (("'" + val + "'") if val else "unset"))
|
|
|
|
|
|
def run_gate(label, path, args):
|
|
if not os.path.exists(path):
|
|
print("\nFAIL: gate '%s' is MISSING — tried %s" % (label, path))
|
|
print(" A missing gate is a failure, never a skip (fail-closed). The reuse-refs")
|
|
print(" checker is shared and lives in the felhom.eu sibling clone; it is never copied.")
|
|
return 1
|
|
print("\n" + "=" * 78)
|
|
print("== gate: %s (%s%s)" % (label, os.path.basename(path),
|
|
(" " + " ".join(args)) if args else ""))
|
|
print("=" * 78, flush=True)
|
|
# stream the gate's own output rather than capturing it — its diagnostics are the point.
|
|
return subprocess.call([sys.executable, path] + args, cwd=CTRL)
|
|
|
|
|
|
def main(argv):
|
|
fast = "--fast" in argv
|
|
unknown = [a for a in argv if a != "--fast"]
|
|
if unknown:
|
|
print("unknown argument(s): %s" % " ".join(unknown))
|
|
print("usage: python3 scripts/controller_gates.py [--fast] (run from controller/)")
|
|
return 2
|
|
|
|
selected = [g for g in GATES if g[3] or not fast]
|
|
skipped = [g[0] for g in GATES if not (g[3] or not fast)]
|
|
print("controller_gates — %d gate(s)%s" % (len(selected), " [--fast]" if fast else ""))
|
|
if skipped:
|
|
print(" --fast SKIPPED (deliberate periodic runs, never in a hook): %s" % ", ".join(skipped))
|
|
hooks_armed_note(REPO)
|
|
|
|
results = [(label, run_gate(label, path, args)) for label, path, args, _f in selected]
|
|
|
|
print("\n" + "=" * 78)
|
|
print("== summary")
|
|
print("=" * 78)
|
|
worst = 0
|
|
for label, rc in results:
|
|
print(" %-18s %-13s (exit %d)" % (label, VERDICT.get(rc, "ERROR"), rc))
|
|
if rc != 0:
|
|
worst = 1 if rc == 1 or worst == 1 else 2
|
|
if worst == 0:
|
|
print("\nall controller gates OK")
|
|
return 0
|
|
convicted = [l for l, rc in results if rc == 1]
|
|
undecided = [l for l, rc in results if rc not in (0, 1)]
|
|
if convicted:
|
|
print("\nCONVICTED: %s" % ", ".join(convicted))
|
|
if undecided:
|
|
print("UNDETERMINED (never a pass): %s" % ", ".join(undecided))
|
|
return worst
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|