3ed5e3e770
gates / gates (push) Successful in 13s
MinAgent: 0.129.0 What was already right: the screen did not bluntly accuse. R-222/R-226 hedged, naming both causes and the kept package, and saying it could not tell them apart. That was honest - and it could not tell them apart because nothing ever looked. Agent v0.129.0 looks, so the hedge becomes an answer. New class RecoveryCodeOpensRetained on HTTP 422, gated by FeatureRetainedRecoveryClass (MinAgent 0.129.0). The gate is SEPARATE from the R-224 one because the two name different agent versions and a box can sit between them, where a 422 is a shape we did not design. ClassifyRecoveryFailure therefore takes both flags; the compiler found every call site. The message says the code is correct, names the supersession date, says the earlier package is kept, and says the CURRENT backups are unaffected - the half a customer will otherwise assume wrong. It promises NO restore: there is no in-product route to a set-aside store (R-312) and the retained package may itself predate the repository-password field. It routes to support, which can do it. The claim guard grew a surface and immediately convicted something. It scanned templates only, while every recovery message is a Go string in a handler - the highest-stakes copy in the product, never scanned. It now scans recovery_handlers.go too, and found a PRE-EXISTING unregistered claim on its first run. Six handler tests asserting which SENTENCE the customer sees; red-proofs asserted applied, including: 422 unconditional makes an agent that never looked read as having looked, and routing 400 to the new class congratulates a mistype.
153 lines
9.3 KiB
Python
153 lines
9.3 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)
|
|
|
|
|
|
def scan():
|
|
convictions, seen_keys = [], set()
|
|
files = sorted(f for f in os.listdir(TEMPLATES) if f.endswith(".html"))
|
|
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())
|