681cc663ef
gates / gates (push) Failing after 13s
Every gate was DECOYED - the label constructed without the fact, the gate run, the verdict recorded.
No verdict here was reached by reading, because reading is exactly how the five prior instances hid.
SCOPE IS A FACT TOO, and it was the big one. Six gates decided what to look at with os.listdir - one
directory level. Every one was green AND CORRECT, because no template subdirectory exists today; every
one would have gone blind the moment anyone added templates/partials/, which is an ordinary act. A
single planted file carrying an emoji, a native confirm(), hand-rolled row markup, a dangling JS id
reference, a templated secret and an unregistered retrieval promise passed all six.
THE CONTROL IS WHAT MAKES THAT A MEASUREMENT: mojibake and docker-v already used os.walk, saw the
identical planted file, and convicted. So the cause was the listing, not the decoy.
COMMENTS ARE NOT CODE, AND COMMENTS ARE NOT CONTROLS. debug-routes matched `case subpath == "x"` in
raw text, so a case left in a commented-out block counted as a live handler - which is R-400's
original defect (seven dead controls on the page an operator opens when something is already wrong)
reached through the one door its own gate could not see. app-row-dedup's MUST_USE check had the same
shape: a commented-out {{template "app_list_row"}} satisfied it.
Stripping is deliberately crude in debug_route_gate, and that is correct there: its own docstring
insists on ten lines that cannot rot. A // inside a string literal truncates that line, which can
only ever HIDE a reference, never invent one - it fails in the safe direction.
NOT FIXED, and left open with its decoy rather than quietly patched: R-425, offbox-rename scans a
fixed three-entry FILES list, so banned NAS branding in a NEW offbox template passes. The scope was
correct when written and silently narrows every time the feature grows a file.
test_gate_decoys.py holds 10 decoys and declares COVERS, which felhom.eu's new decoy-coverage gate
AST-parses - a substring search for coverage would be the very shape this sweep exists to find.
No Go code. No version bump. No image. No golden owed.
Survey: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
165 lines
9.8 KiB
Python
165 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
"""retrieval_promise_gate — pin the CLAIM, not the word (R-302).
|
|
|
|
WHY THIS IS NOT A STRING BAN. Five instances of "you can get your old backups back with your recovery
|
|
code" have surfaced ONE AT A TIME (R-294, R-299, and the two R-302 fixed this session), each found only
|
|
after the previous one was fixed. The obvious guard — forbid the sentence — was tried twice and failed
|
|
twice:
|
|
|
|
* v0.211.0 asserted the SINGULAR „visszaállítható lehet"; the card carried the PLURAL
|
|
„visszaállíthatók lehetnek" one paragraph above it and walked straight past (R-299).
|
|
* Broadening to the stem `visszaállíthat` then missed the banner entirely, because the banner says
|
|
„visszaszerezheted" — a different verb for the same claim.
|
|
|
|
AND THE STEM CANNOT BE BANNED. The honest replacement copy this session ships *contains the stem*:
|
|
„Hogy ezek még visszaszerezhetők-e … azt innen nem tudjuk megállapítani" is a QUESTION about
|
|
retrievability, and it is the correct sentence. A guard that forbade the stem would force the product
|
|
to avoid a normal Hungarian verb — a guard shaping the product around itself.
|
|
|
|
SO: every occurrence of a retrieval stem in a customer-facing template must be REGISTERED here with a
|
|
reason. Unregistered occurrences fail. The failure mode this actually catches is the real one — a new
|
|
claim appearing somewhere nobody was looking — without pretending a word is a claim.
|
|
|
|
Go template comments ({{/* … */}}) are stripped before scanning: html/template never renders them, so
|
|
prose explaining a fix is not a claim. HTML <!-- --> comments DO ship and are deliberately scanned.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
TEMPLATES = os.path.join(_HERE, "..", "internal", "web", "templates")
|
|
|
|
# R-311 — THE GATE HAD A BLIND SPOT THE SIZE OF THE RECOVERY SCREEN.
|
|
#
|
|
# It scanned `internal/web/templates` only. But every one of the recovery screen's messages is a Go
|
|
# STRING in a handler, not template text — including the four R-224 messages and the R-222/R-226 one,
|
|
# i.e. the highest-stakes customer copy in the product, on the one screen whose whole purpose is to be
|
|
# believed about someone's backups. None of it had ever been scanned.
|
|
#
|
|
# Go `//` and `/* */` comments are stripped for the same reason template comments are: they never
|
|
# reach a customer. (A `//` inside a Hungarian string literal would be stripped too — there are none,
|
|
# and a false NEGATIVE there is the safe direction for a guard that convicts on presence.)
|
|
GO_SOURCES = [
|
|
os.path.join(_HERE, "..", "internal", "web", "recovery_handlers.py".replace(".py", ".go")),
|
|
]
|
|
|
|
# The verbs that carry the claim "your old backups can be got back".
|
|
# R-311 adds `visszanyit`: the honest new message says a customer needs support's help „a régebbi
|
|
# előzményed visszanyitásához". That is the SAME claim in a fourth verb, and the docstring above
|
|
# records what happens when the guard chases words instead of claims — it misses the next one.
|
|
STEMS = ["visszaállíthat", "visszaszerezhet", "visszahozhat", "visszanyit"]
|
|
|
|
# (template, substring that identifies the occurrence) -> why it is allowed.
|
|
# The substring must be specific enough that a DIFFERENT claim in the same file does not match it.
|
|
ALLOWLIST = {
|
|
("backups.html", "amelyből az egész készülék visszaállítható"):
|
|
"the LOCAL whole-device backup, made and held by the host agent. Nothing to do with the "
|
|
"off-site escrow claim — no recovery code is involved.",
|
|
("backups_apps.html", "alkalmazásonként visszaállítható"):
|
|
"per-app restore from the local app-data backup. Same: local, no recovery code.",
|
|
("backups_remote.html", "mentéseid visszaszerezhetők.</strong>"):
|
|
"the RecoveryOffer entry point. TRUE where it renders: it is gated on the hub telling this box "
|
|
"it holds a sealed package for it, which is the claim being made. Deliberately left alone.",
|
|
("backups_remote.html", "a mentéseid visszaszerezhetők, és a törlés elmarad."):
|
|
"the abandon block's promise — R-302 made it conditional on AbandonRetrievalOffered; this is "
|
|
"the TRUE branch.",
|
|
("backups_remote.html", "Hogy ezek még visszaszerezhetők-e"):
|
|
"R-302's cautious branch. Contains the stem inside a QUESTION about knowability — the sentence "
|
|
"the gate exists to protect, not to forbid.",
|
|
("layout.html", "Addig még visszaszerezheted őket a helyreállítási kóddal."):
|
|
"the banner's promise — R-302 made it conditional on RecoveryAbandonRetrievalOffered; TRUE branch.",
|
|
("layout.html", "Hogy ezek még visszaszerezhetők-e"):
|
|
"R-302's cautious branch on the banner. As above.",
|
|
("recovery_handlers.go", "A régi előzmény visszanyitása felülírná azt"):
|
|
"PRE-EXISTING and never scanned until R-311 extended this gate to Go handlers — which is the "
|
|
"point of extending it. It is NOT a promise: it is the reason for a REFUSAL (RecoverRefused, "
|
|
"a different repository password is already present), i.e. the sentence says the reopening "
|
|
"would overwrite and was therefore not done. Registered as an explanation, not a claim.",
|
|
("recovery_handlers.go", "A régebbi előzményed visszanyitásához a Felhom ügyfélszolgálatának segítsége kell"):
|
|
"R-311's truthful message for a code that opens a RETAINED package. It is a claim, and it is "
|
|
"TRUE: the drill of 2026-08-12 recovered exactly this by hand (unsealed the retained package, "
|
|
"opened the set-aside store, restored planted files byte-identical). It routes to SUPPORT "
|
|
"rather than to a button precisely because there is no in-product route yet — the restore "
|
|
"machinery resolves its repository from settings and its password from one file. If that route "
|
|
"is ever built, this entry changes; if support ever cannot do it, this sentence must go.",
|
|
("recovery.html", "a mentéseid visszaszerezhetők, és a törlés elmarad;"):
|
|
"the abandon CONFIRMATION screen, shown at the moment of the decision. True by construction "
|
|
"there: the package the hub holds right now is the one about to be pinned. Left alone.",
|
|
}
|
|
|
|
TEMPLATE_COMMENT = re.compile(r"\{\{/\*.*?\*/\}\}", re.S)
|
|
GO_COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
|
|
|
|
|
|
# R-421 (2026-09-01): any depth, was os.listdir (one level). No template subdirectory exists
|
|
# today, so this was green and correct — and would have stayed green the moment anyone added
|
|
# templates/partials/. Measured: a planted file there passed every listdir-based gate.
|
|
def _html_at_any_depth(root):
|
|
out = []
|
|
for dirpath, _dirs, names in os.walk(root):
|
|
for fn in sorted(names):
|
|
if fn.endswith('.html'):
|
|
out.append(os.path.join(dirpath, fn))
|
|
return sorted(out)
|
|
|
|
|
|
def scan():
|
|
convictions, seen_keys = [], set()
|
|
files = [os.path.relpath(x, TEMPLATES) for x in _html_at_any_depth(TEMPLATES)]
|
|
sources = [(f, os.path.join(TEMPLATES, f), TEMPLATE_COMMENT) for f in files]
|
|
for gp in GO_SOURCES:
|
|
if not os.path.exists(gp):
|
|
raise SystemExit(f"retrieval-promise gate: declared Go source is missing: {gp}")
|
|
sources.append((os.path.basename(gp), gp, GO_COMMENT))
|
|
files = files + [os.path.basename(gp)]
|
|
for name, path, stripper in sources:
|
|
text = stripper.sub("", open(path, encoding="utf-8").read())
|
|
for stem in STEMS:
|
|
for m in re.finditer(re.escape(stem) + r"[a-záéíóöőúüű]*", text):
|
|
line = text[: m.start()].count("\n") + 1
|
|
# SPAN-based, not window-based. The promise and the cautious disclaimer sit within a
|
|
# hundred characters of each other in the same paragraph, so a proximity window matches
|
|
# whichever key it tries first and reports the other as stale — which is exactly what a
|
|
# first draft of this gate did. An occurrence belongs to an entry only if it falls
|
|
# INSIDE that entry's own text.
|
|
hit = None
|
|
for k in (k for k in ALLOWLIST if k[0] == name):
|
|
for om in re.finditer(re.escape(k[1]), text):
|
|
if om.start() <= m.start() and m.end() <= om.end():
|
|
hit = k
|
|
break
|
|
if hit:
|
|
break
|
|
if hit:
|
|
seen_keys.add(hit)
|
|
else:
|
|
ctx = text[max(0, m.start() - 100): m.end() + 100]
|
|
convictions.append((name, line, m.group(0), " ".join(ctx.split())[:160]))
|
|
return files, convictions, seen_keys
|
|
|
|
|
|
def main():
|
|
files, convictions, seen = scan()
|
|
stale = [k for k in ALLOWLIST if k not in seen]
|
|
for name, line, word, ctx in convictions:
|
|
print(f" {name}:{line} unregistered retrieval claim ({word}):\n …{ctx}…")
|
|
for k in stale:
|
|
print(f" STALE ALLOWLIST ENTRY (no longer present): {k[0]} :: {k[1]!r}")
|
|
if convictions or stale:
|
|
print(f"\nRETRIEVAL-PROMISE GATE FAILED: {len(convictions)} unregistered, {len(stale)} stale, "
|
|
f"across {len(files)} template(s).")
|
|
print("Five instances of this claim have surfaced one at a time. If the new text is a genuine")
|
|
print("claim, make it CONDITIONAL on what the box can see; if it is a question about")
|
|
print("knowability, or an unrelated local-backup sentence, add it to ALLOWLIST with the reason.")
|
|
return 1
|
|
print(f"retrieval-promise gate OK — {len(files)} surface(s) incl. {len(GO_SOURCES)} Go handler file(s), "
|
|
f"{len(ALLOWLIST)} registered claim(s), none unregistered")
|
|
print(" (BLIND SPOT: it registers WHERE the claim is made, not whether each conditional is wired")
|
|
print(" to a true predicate — that is what the R-302 render tests are for.)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|