R-224/R-226 Part 1: why the unlock failed decides what we say
The failure branch was a two-way choice — superseded? M4 : M1 — and BOTH are statements about the customer's code. rerr was never inspected, so a hub that refused, an agent that was stopped and a genuinely mistyped code all produced the same accusation. Measured live 2026-08-05 with a CORRECT current code: hub firewalled off 0.0556s, agent stopped 0.0299s, against ~1.0s for a real unseal. Five classes, from the VALUE and never the text: hub-unreachable 502/503 from the agent — the code was NOT used agent-unreachable no agent verdict at all (transport) — NOT used no-bundle 404 bundle-too-old 409 asked-and-refused 400 — the ONLY class that may mention typing unknown everything else -> NEUTRAL, the safe default agentapi.RecoveryRefusal carries the status as a value (refusalError flattened it into a sentence, and a sentence is not something a caller can branch on). THE OLD-AGENT CASE IS WHY THIS NEEDS A COUPLING. Agent < 0.126.0 answers 400 for both a fetch failure and a wrong code, so a 400 from one cannot be read as a refusal. FeatureRecoveryFailureClass (MinAgent 0.126.0) withholds that reading and the 400 degrades to neutral. The gate BLOCKS NOTHING — it only decides whether the customer may be told to check their typing. R-226: the superseded message now names BOTH possibilities and restores the ten-words prompt. The two are indistinguishable at the engine; the honest message says so. It still does not promise the earlier package can be opened. Elapsed time is logged (it is what diagnosed this) and is NEVER a classifier. Tests: scenarios A-E at the HANDLER + the classifier table asserting the same sentence under two statuses classifies two ways. Red-proofs, each demonstrated failing then restored: delete the 502 case (A), remove the mistype clause (C), default to the accusation (D), route an instant transport failure to the typing message (E). Two existing tests encoded the defect and were corrected, not deleted: the web fake returned a BARE error for 'wrong code' (which is the shape of a failure we cannot classify), and R-222's test forbade any mention of typing on a superseded box — half of which R-226 deliberately reverses. 28 packages ok, vet clean, all controller gates OK.
This commit is contained in:
@@ -55,6 +55,21 @@ const FeatureBackupAgeState Feature = "backup_age_state"
|
||||
// never have worked is attributed to the code.
|
||||
const FeatureOffsiteKeyRecovery Feature = "offsite_key_recovery"
|
||||
|
||||
// FeatureRecoveryFailureClass is agent v0.126.0's SPLIT of a failed unlock into distinguishable
|
||||
// statuses (R-224): 502 the sealed bundle could not be FETCHED · 400 it was fetched and the code was
|
||||
// refused · 404 no bundle · 409 the bundle predates the repository-password field.
|
||||
//
|
||||
// ⚠ WHAT THIS GATE ACTUALLY GUARDS is the meaning of **400**, and nothing else. An agent older than
|
||||
// v0.126.0 answers 400 for BOTH a fetch failure and a wrong code — one status, one sentence, two
|
||||
// situations — so on such an agent a 400 cannot be read as "the code was refused". It means "one of
|
||||
// two things and we cannot tell which", which is `RecoveryUnknown`, which is neutral.
|
||||
//
|
||||
// So this gate does not block anything and has no fail-closed behaviour to get wrong: the unlock is
|
||||
// attempted either way (FeatureOffsiteKeyRecovery already decides THAT). It only decides whether the
|
||||
// customer may be told to check their typing. Unknown → they may not. **That is the safe direction,
|
||||
// and it heals itself the moment the agent updates.**
|
||||
const FeatureRecoveryFailureClass Feature = "recovery_failure_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
|
||||
@@ -122,6 +137,10 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
|
||||
FeatureOffsiteKeyRecovery: func(ctx context.Context, p SupportProber) error {
|
||||
return errNoRecoveryProbe
|
||||
},
|
||||
// Same POST route, same reason it cannot be probed — the decision falls to the VERSION path.
|
||||
FeatureRecoveryFailureClass: func(ctx context.Context, p SupportProber) error {
|
||||
return errNoRecoveryProbe
|
||||
},
|
||||
}
|
||||
|
||||
// errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be
|
||||
@@ -145,6 +164,9 @@ var featureMinAgent = map[Feature]string{
|
||||
// R-199 links 7–8: POST /escrow/recover-offsite-password. R-216 — this row is the whole reason a
|
||||
// correct recovery code can no longer be reported as wrong on an agent that cannot answer.
|
||||
FeatureOffsiteKeyRecovery: "0.125.0",
|
||||
|
||||
// R-224 — the four-way status split of a failed unlock.
|
||||
FeatureRecoveryFailureClass: "0.126.0",
|
||||
}
|
||||
|
||||
// MinAgentFor returns the declared minimum agent version for a feature ("" when the feature has no
|
||||
|
||||
Reference in New Issue
Block a user