v0.214.0 — the recovery screen stops hedging about a code it can now check (R-311)
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.
This commit is contained in:
2026-08-12 18:42:02 +02:00
parent 3168a78935
commit 3ed5e3e770
10 changed files with 365 additions and 11 deletions
+8
View File
@@ -200,6 +200,14 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated
per-visit, the durable opt-out silences the bar only, and **none of the three removes the entry
point on Távoli mentés**. While a recovery is outstanding, „Helyreállítási kód létrehozása" is
UNAVAILABLE — creating a new code would put the earlier history beyond every shipped path.
**A code for an EARLIER package is now recognised as correct (v0.214.0, R-311; MinAgent 0.129.0).**
When the supplied code does not open the package the hub currently holds, the agent tries the
RETAINED ones and answers 422 if one opens; the screen then says the code is *correct*, names the
supersession date, says the earlier package is kept and that the CURRENT backups are unaffected, and
routes to support. It deliberately 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.
On an agent older than 0.129.0 the screen keeps the earlier hedged wording, which was honest: until
something looked, a correct-but-earlier code and a mistype really were indistinguishable.
(traefik/cloudflared/filebrowser) get curated Hungarian display identity from the
`inframeta.go` map (name + description + generic `/static/infra-logo.svg` fallback icon);
filebrowser is the only infra stack with a customer link (`files.<domain>`).
+24 -1
View File
@@ -201,6 +201,15 @@ const (
// agent verdict at all. **The code was not used.** Distinct from RecoveryHubUnreachable because
// it is a different fault, with different words and a different remedy.
RecoveryAgentUnreachable
// RecoveryCodeOpensRetained — the code was used, it WORKED, and it opened a RETAINED earlier
// package rather than the one currently held (R-311, agent >= v0.129.0).
//
// **The customer is not at fault here and must not be told they might be.** This class exists
// because until 2026-08-12 this situation and a mistype were indistinguishable: both fail closed
// against the current package, and nothing ever tried the retained ones. The screen said as much
// out loud — a true sentence about our own incuriosity that a customer reads as a statement about
// their code.
RecoveryCodeOpensRetained
)
// ClassifyRecoveryFailure maps an unlock error to its class, from the VALUE and never the text.
@@ -210,7 +219,11 @@ const (
// "the code was refused" — it means "one of two things, and we cannot tell which". Pass false there
// and the 400 degrades to RecoveryUnknown, which is neutral. That degradation is the point: it is
// safe, it is silent, and it heals itself when the agent updates.
func ClassifyRecoveryFailure(err error, trustRefusal bool) RecoveryFailure {
// ⚠ `trustRetained` is the R-311 twin of `trustRefusal` and is separate on purpose: the two gates
// name different agent versions (v0.126.0 and v0.129.0) and a box can sit between them. Passing
// `trustRefusal` for both would let a v0.126128 agent's unexpected 422 be read as a verdict it
// cannot produce.
func ClassifyRecoveryFailure(err error, trustRefusal, trustRetained bool) RecoveryFailure {
if err == nil {
return RecoveryUnknown
}
@@ -230,6 +243,14 @@ func ClassifyRecoveryFailure(err error, trustRefusal bool) RecoveryFailure {
return RecoveryNoBundle
case http.StatusConflict:
return RecoveryBundleTooOld
case http.StatusUnprocessableEntity:
// R-311. Gated on the SAME trust flag as 400, and for the mirror-image reason: an agent that
// predates the retained lookup cannot emit 422 at all, so a 422 from anywhere else is a shape
// we did not design and must not be read as a statement about the customer's code.
if trustRetained {
return RecoveryCodeOpensRetained
}
return RecoveryUnknown
case http.StatusBadRequest:
if trustRefusal {
return RecoveryAskedAndRefused
@@ -253,6 +274,8 @@ func (f RecoveryFailure) String() string {
return "no-bundle"
case RecoveryBundleTooOld:
return "bundle-too-old"
case RecoveryCodeOpensRetained:
return "code-opens-retained"
default:
return "unknown"
}
+25
View File
@@ -70,6 +70,19 @@ const FeatureOffsiteKeyRecovery Feature = "offsite_key_recovery"
// and it heals itself the moment the agent updates.**
const FeatureRecoveryFailureClass Feature = "recovery_failure_class"
// FeatureRetainedRecoveryClass is agent v0.129.0's FIFTH status on a failed unlock (R-311): 422, the
// code is correct and opens a RETAINED earlier package rather than the current one.
//
// ⚠ WHAT THIS GATE GUARDS is whether the screen may say WHICH of the two causes it is. Before
// v0.129.0 nothing ever tried the retained packages, so a correct-but-earlier code and a mistype were
// genuinely indistinguishable and the screen said so. That sentence was HONEST then and becomes a
// falsehood the moment the agent can tell them apart — so the gate decides which of two true
// sentences to print, never whether to attempt the unlock.
//
// Unknown → the older, hedged sentence. That is the safe direction: it claims less, it was correct
// for two months, and it heals itself when the agent updates.
const FeatureRetainedRecoveryClass Feature = "retained_recovery_class"
// SupportState is a probe verdict. The zero value is SupportUnknown (fail-open: unknown never
// refuses — the existing agent-error paths speak honestly when the agent is down).
type SupportState int
@@ -141,6 +154,11 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
FeatureRecoveryFailureClass: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe
},
// R-311, same route and same reason. The row must exist or SupportsWithSource returns
// "unregistered"/SupportUnknown on the table gap and the version row is never consulted.
FeatureRetainedRecoveryClass: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe
},
}
// errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be
@@ -167,6 +185,13 @@ var featureMinAgent = map[Feature]string{
// R-224 — the four-way status split of a failed unlock.
FeatureRecoveryFailureClass: "0.126.0",
// R-311 — the FIFTH status: 422, "your code is correct, it opens an EARLIER package". Before
// v0.129.0 the agent never looked at retained packages, so this situation was indistinguishable
// from a mistype and arrived as 400. An older agent therefore cannot produce a 422 at all, and the
// screen must keep saying it cannot tell the two apart — which was true, and is what this gate
// preserves for boxes that have not updated yet.
FeatureRetainedRecoveryClass: "0.129.0",
}
// MinAgentFor returns the declared minimum agent version for a feature ("" when the feature has no
@@ -231,14 +231,14 @@ func TestClassifyRecoveryFailure_MapsFromTheValueNotTheText(t *testing.T) {
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := agentapi.ClassifyRecoveryFailure(tc.err, tc.trusted); got != tc.want {
if got := agentapi.ClassifyRecoveryFailure(tc.err, tc.trusted, tc.trusted); got != tc.want {
t.Fatalf("got %v, want %v", got, tc.want)
}
})
}
// The text must be irrelevant: the SAME sentence under two statuses classifies two ways.
same := "the recovery code did not open the sealed bundle"
if agentapi.ClassifyRecoveryFailure(refusal(400, same), true) == agentapi.ClassifyRecoveryFailure(refusal(502, same), true) {
if agentapi.ClassifyRecoveryFailure(refusal(400, same), true, true) == agentapi.ClassifyRecoveryFailure(refusal(502, same), true, true) {
t.Fatal("classification followed the TEXT — it must follow the status")
}
}
+50 -1
View File
@@ -269,6 +269,31 @@ func (s *Server) SetRecoveryRefusalTrusted(fn func(context.Context) bool) {
s.recoveryRefusalTrustedFn = fn
}
// recoveryRetainedTrusted reports whether a 422 from the agent may be read as "the code is correct
// and opens a RETAINED earlier package" (R-311).
//
// Only agent >= v0.129.0 ever looks at retained packages, so only it can produce that verdict. On
// anything older the two causes really are indistinguishable and the screen must keep saying so.
//
// ⚠ Like its R-224 twin this gate BLOCKS NOTHING — the unlock is attempted either way. It decides
// only which of two TRUE sentences the customer reads, and "not sure" picks the one that claims less.
func (s *Server) recoveryRetainedTrusted(ctx context.Context) bool {
if s.recoveryRetainedTrustedFn != nil {
return s.recoveryRetainedTrustedFn(ctx)
}
agent, err := s.agentClient()
if err != nil {
return false
}
state, _ := s.netFeatures.SupportsWithSource(ctx, agent, agentapi.FeatureRetainedRecoveryClass)
return state == agentapi.SupportYes
}
// SetRecoveryRetainedTrusted overrides the R-311 version gate (tests). INIT-ONLY.
func (s *Server) SetRecoveryRetainedTrusted(fn func(context.Context) bool) {
s.recoveryRetainedTrustedFn = fn
}
// recoveryNow is the clock the unlock path measures itself against. Real time in production; tests
// inject so §7.2's guard — the typing message may only follow a REAL unseal — can be asserted
// without sleeping. It is an observability seam and a test seam: **it must never become a
@@ -373,7 +398,7 @@ func (s *Server) recoveryUnlockHandler(w http.ResponseWriter, r *http.Request) {
// The duration is logged because it is what DIAGNOSED this and it is the cheapest possible
// tell for the operator — but it is NEVER the classifier. Time is a symptom; the status is
// the fact.
class := agentapi.ClassifyRecoveryFailure(rerr, s.recoveryRefusalTrusted(r.Context()))
class := agentapi.ClassifyRecoveryFailure(rerr, s.recoveryRefusalTrusted(r.Context()), s.recoveryRetainedTrusted(r.Context()))
s.logger.Printf("[WARN] [web] recovery: unlock failed after %s (class=%s): %v", unsealTook.Round(time.Millisecond), class, rerr)
switch class {
case agentapi.RecoveryHubUnreachable:
@@ -397,6 +422,30 @@ func (s *Server) recoveryUnlockHandler(w http.ResponseWriter, r *http.Request) {
// The code WORKED — the bundle opened. It simply predates the field we need.
s.renderRecovery(w, r, "A kódod megnyitotta a csomagot, de az még nem tartalmazza a házon kívüli tárhely kulcsát — régebben készült, mint amikor ezt elkezdtük belerakni, és utólag nem pótolható. A kódoddal semmi baj. Keresd a Felhom ügyfélszolgálatát.", "", nil)
return
case agentapi.RecoveryCodeOpensRetained:
// ── R-311 — THE CODE IS CORRECT, AND WE CHECKED. ──────────────────────────────────
//
// The agent tried the retained packages and one of them opened. This is no longer an
// inference from "the hub says an earlier package exists" (R-222) — it is a measurement,
// and it is the difference between hedging and knowing.
//
// What this message may NOT do: promise the older history can be reopened. The retained
// package may itself predate the repository-password field, and there is no route from
// this screen to a set-aside store in any case (the restore machinery resolves its
// repository from settings and its password from one file — see the session's spike). A
// conditional promise that turns out false HERE is worse than saying less; that is the
// R-202 lesson and it applies with full force to a screen about someone's backups.
//
// So it states what is known, denies what the customer will otherwise assume (that their
// current backups are affected), and names a route. A refusal that names no route is a
// defect on this surface.
when := ""
if _, at := s.recoverySuperseded(); at != "" {
when = " (" + at + ")"
}
s.logger.Printf("[INFO] [web] recovery: the code opened a RETAINED package — the customer is not at fault")
s.renderRecovery(w, r, "A kódod helyes, de egy korábbi csomagot nyit meg, nem azt, amit most őrzünk ehhez a géphez. A géped időközben új mentési kulcsot kapott. A korábbi csomagot"+when+" nem töröltük, megőrizzük — a mostani mentéseidet ez nem érinti, azokkal semmi nem történt. A régebbi előzményed visszanyitásához a Felhom ügyfélszolgálatának segítsége kell: írj nekik, és add meg, hogy a régi mentéseidhez szeretnél hozzáférni. A kódodat tedd el, szükség lesz rá.", "", nil)
return
case agentapi.RecoveryAskedAndRefused:
// The bundle was fetched and the code did not open it. THIS is the only class from which
// the customer may be told to check their typing — see the two messages below.
@@ -0,0 +1,167 @@
package web
import (
"context"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
)
// ── R-311 — A CORRECT CODE FOR AN EARLIER PACKAGE STOPS BEING CALLED WRONG ───────────────────────
//
// What was measured on 2026-08-12: a recovery code that provably opens a RETAINED package — unsealed
// by hand, and it restored planted files byte-identical from a store the box itself could no longer
// open — was answered by the product as a code that opened nothing.
//
// The screen already hedged (R-222/R-226): "it may be a typo, or it may be an older code, and we
// cannot tell them apart from here." That sentence was TRUE and it was honest. It was also a
// statement about our own incuriosity — nothing ever tried the retained packages — read by the
// customer as a statement about their code. Now something tries, so the hedge can become an answer.
//
// Every test asserts the EFFECT at the HANDLER: which sentence the customer is shown.
// saysCodeIsCorrect is the load-bearing half of the new message. Anything that leaves this out has
// failed at the one job it has.
func saysCodeIsCorrect(body string) bool { return strings.Contains(body, "A kódod helyes") }
// saysCurrentBackupsUnaffected — a customer told "your code opens something else" will otherwise
// assume their CURRENT backups are in doubt. They are not, and the message must say so.
func saysCurrentBackupsUnaffected(body string) bool {
return strings.Contains(body, "mostani mentéseidet ez nem érinti")
}
// namesARoute — a refusal that names no next step is a defect on this surface.
func namesARoute(body string) bool { return strings.Contains(body, "ügyfélszolgálat") }
// ── THE ONE THAT MATTERS ────────────────────────────────────────────────────────────────────────
//
// RED-PROOF: delete the `agentapi.RecoveryCodeOpensRetained` case from recoveryUnlockHandler so a 422
// falls through → the customer gets the hedged "we cannot tell them apart" text (or, without R-222's
// gate, the bare typing accusation) → this FAILS on saysCodeIsCorrect. That is yesterday's behaviour
// returning, and it is the whole reason this case exists.
func TestR311_CodeOpensRetained_IsNotReportedAsWrong(t *testing.T) {
f := newRecoveryFixture(t)
f.rec.failWith = refusal(422, "the recovery code is correct, but it belongs to an EARLIER sealed package (superseded 2026-08-12 15:18:55), not the one currently held")
body := postUnlockWith(t, f.s, testRecoveryCode).Body.String()
if namesTyping(body) || isBareAccusation(body) {
t.Fatalf("a CORRECT code was reported as a possible mistype — the R-311 defect is back; got %q", firstAlert(body))
}
if !saysCodeIsCorrect(body) {
t.Fatalf("the message must say the code is CORRECT; got %q", firstAlert(body))
}
if !saysCurrentBackupsUnaffected(body) {
t.Errorf("the message must deny the assumption it creates — that the CURRENT backups are affected; got %q", firstAlert(body))
}
if !namesARoute(body) {
t.Errorf("a refusal on this screen must name a route; got %q", firstAlert(body))
}
}
// The message must NOT promise the older history can be reopened from this screen. There is no
// in-product route to a set-aside store (the restore machinery resolves its repository from settings
// and its password from one file), and the retained package may itself predate the repository-password
// field. A conditional promise that turns out false HERE is worse than saying less — the R-202 lesson.
//
// RED-PROOF: add "és vissza is állítjuk" (or any unconditional retrieval promise) to the message →
// this FAILS.
func TestR311_TheMessageDoesNotPromiseARestore(t *testing.T) {
f := newRecoveryFixture(t)
f.rec.failWith = refusal(422, "belongs to an EARLIER sealed package")
body := firstAlert(postUnlockWith(t, f.s, testRecoveryCode).Body.String())
// A promise would take the form "we will restore it" / "you can get them back from here".
for _, promise := range []string{"vissza is állítjuk", "most visszaállítjuk", "innen visszaszerezheted", "azonnal visszaállítható"} {
if strings.Contains(body, promise) {
t.Fatalf("the message promises a restore this screen cannot perform (%q): %q", promise, body)
}
}
// It must still say the package is KEPT — that is the true, useful half.
if !strings.Contains(body, "nem töröltük") {
t.Errorf("the message must say the earlier package was kept; got %q", body)
}
}
// AN OLDER AGENT CANNOT PRODUCE THIS VERDICT, so the screen must keep the hedged sentence. The gate
// decides which of two TRUE sentences to print, never whether to attempt the unlock.
//
// RED-PROOF: make ClassifyRecoveryFailure return RecoveryCodeOpensRetained for 422 regardless of
// trustRetained → an agent that never looked at a retained package is treated as having looked →
// this FAILS.
func TestR311_OlderAgent_KeepsTheHedgedSentence(t *testing.T) {
f := newRecoveryFixture(t)
f.s.SetRecoveryRetainedTrusted(func(context.Context) bool { return false })
f.rec.failWith = refusal(422, "some shape this agent should not be producing")
body := postUnlockWith(t, f.s, testRecoveryCode).Body.String()
if saysCodeIsCorrect(body) {
t.Fatalf("an agent too old to look at retained packages was read as having looked; got %q", firstAlert(body))
}
// RecoveryUnknown → the neutral message. It must not accuse either.
if isBareAccusation(body) {
t.Errorf("an unclassifiable 422 produced an accusation; got %q", firstAlert(body))
}
}
// SCENARIO C — a genuinely wrong code must still reach the typing message. The new branch must not
// become an escape hatch that stops the product ever saying "check your typing".
//
// RED-PROOF: route 400 to RecoveryCodeOpensRetained → a mistype is congratulated → this FAILS.
func TestR311_WrongCodeStillReachesTheTypingMessage(t *testing.T) {
f := newRecoveryFixture(t)
f.rec.failWith = refusal(400, "the recovery code did not open the sealed bundle — nothing was written")
body := postUnlockWith(t, f.s, testRecoveryCode).Body.String()
if saysCodeIsCorrect(body) {
t.Fatalf("a WRONG code was told it is correct; got %q", firstAlert(body))
}
if !namesTyping(body) {
t.Errorf("a genuine refusal must still be able to mention typing; got %q", firstAlert(body))
}
}
// SCENARIO A — the ordinary successful recovery is untouched. This is the path that WORKS, and it is
// the one a change like this is most likely to break by accident.
//
// RED-PROOF: return the 422 refusal from the fixture's recoverer on the success path → this FAILS.
func TestR311_OrdinaryRecoveryUnaffected(t *testing.T) {
f := newRecoveryFixture(t)
f.rec.failWith = nil // the current package opens
rr := postUnlockWith(t, f.s, testRecoveryCode)
body := rr.Body.String()
if saysCodeIsCorrect(body) {
t.Fatal("the retained message rendered on a SUCCESSFUL recovery")
}
if namesTyping(body) || isBareAccusation(body) {
t.Fatalf("a successful recovery produced a refusal; got %q", firstAlert(body))
}
}
// The classifier maps 422 by STATUS and gate, never by sentence — the R-224 rule, applied to the new
// status so it cannot regress the same way.
//
// RED-PROOF: classify on strings.Contains(reason, "EARLIER") instead of the status → the two
// same-text/different-status calls below stop differing → this FAILS.
func TestR311_ClassificationFollowsStatusNotText(t *testing.T) {
same := "the recovery code is correct, but it belongs to an EARLIER sealed package"
if agentapi.ClassifyRecoveryFailure(refusal(422, same), true, true) == agentapi.ClassifyRecoveryFailure(refusal(400, same), true, true) {
t.Fatal("classification followed the TEXT — it must follow the status")
}
if got := agentapi.ClassifyRecoveryFailure(refusal(422, same), true, true); got != agentapi.RecoveryCodeOpensRetained {
t.Fatalf("422 with the gate open = %v, want RecoveryCodeOpensRetained", got)
}
if got := agentapi.ClassifyRecoveryFailure(refusal(422, same), true, false); got != agentapi.RecoveryUnknown {
t.Fatalf("422 with the gate CLOSED = %v, want RecoveryUnknown (neutral)", got)
}
// The two gates are independent: an agent between v0.126.0 and v0.129.0 trusts 400 but not 422.
if got := agentapi.ClassifyRecoveryFailure(refusal(400, same), true, false); got != agentapi.RecoveryAskedAndRefused {
t.Fatalf("400 with only the R-224 gate open = %v, want RecoveryAskedAndRefused", got)
}
}
+3
View File
@@ -147,6 +147,9 @@ func newRecoveryFixture(t *testing.T) *recoveryFixture {
// R-224: the fixture's agent is a current one, so a 400 may be read as a genuine refusal. Tests
// that need the OLD-agent behaviour override this explicitly.
s.SetRecoveryRefusalTrusted(func(context.Context) bool { return true })
// R-311: likewise a current agent, so a 422 may be read as "the code opens a RETAINED package".
// The old-agent behaviour has its own test, which overrides this.
s.SetRecoveryRetainedTrusted(func(context.Context) bool { return true })
return &recoveryFixture{s: s, mgr: mgr, sett: sett, rec: rec, runner: rr, dataDir: cfg.Paths.DataDir}
}
+3
View File
@@ -116,6 +116,9 @@ type Server struct {
// recoveryRefusalTrustedFn overrides the R-224 gate deciding whether a 400 may be read as a
// genuine refusal (tests). nil → the agent-version path.
recoveryRefusalTrustedFn func(context.Context) bool
// recoveryRetainedTrustedFn overrides the R-311 gate deciding whether a 422 may be read as "the
// code is correct and opens a RETAINED earlier package" (tests). INIT-ONLY.
recoveryRetainedTrustedFn func(context.Context) bool
// recoveryNowFn is the unlock path's clock (tests inject; nil → time.Now). Observability and
// tests only — never a classifier.
recoveryNowFn func() time.Time
+43 -7
View File
@@ -27,10 +27,28 @@ import os
import re
import sys
TEMPLATES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "internal", "web", "templates")
_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".
STEMS = ["visszaállíthat", "visszaszerezhet", "visszahozhat"]
# 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.
@@ -53,20 +71,38 @@ ALLOWLIST = {
"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"))
for name in files:
path = os.path.join(TEMPLATES, name)
text = TEMPLATE_COMMENT.sub("", open(path, encoding="utf-8").read())
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
@@ -105,8 +141,8 @@ def main():
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)} templates, {len(ALLOWLIST)} registered claim(s), "
f"none unregistered")
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