The escrow wizard sends the typed-back words to the box; the new package reaches the hub only when they match (R-1151 controller half, unreleased; works with every agent)
gates / gates (push) Successful in 47s

This commit is contained in:
2026-10-19 03:19:02 +02:00
parent 8ae9178138
commit ba4a6f060b
12 changed files with 434 additions and 35 deletions
+62 -5
View File
@@ -32,7 +32,8 @@ type escrowAgent interface {
EscrowPreflight(ctx context.Context) (agentapi.EscrowPreflightResponse, error)
EscrowCeremonyStart(ctx context.Context) (agentapi.EscrowCeremonyStartResponse, int, error)
EscrowCeremonyStatus(ctx context.Context) (agentapi.EscrowCeremonyStatusResponse, error)
EscrowCeremonyClaim(ctx context.Context) (string, int, error)
EscrowCeremonyClaim(ctx context.Context) (agentapi.EscrowClaim, int, error)
EscrowCeremonyConfirm(ctx context.Context, words []string) (int, error)
AgentVersion() string
}
@@ -102,6 +103,8 @@ func (s *Server) ServeEscrowAPI(w http.ResponseWriter, r *http.Request) {
s.escrowStatusAPIHandler(w, r)
case r.URL.Path == "/api/escrow/claim" && r.Method == http.MethodPost:
s.escrowClaimAPIHandler(w, r)
case r.URL.Path == "/api/escrow/confirm" && r.Method == http.MethodPost:
s.escrowConfirmAPIHandler(w, r)
default:
escrowJSON(w, http.StatusNotFound, nil, "ismeretlen végpont")
}
@@ -275,7 +278,8 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) {
escrowJSON(w, http.StatusBadGateway, nil, "Az ügynök nem elérhető.")
return
}
code, status, err := agent.EscrowCeremonyClaim(r.Context())
claim, status, err := agent.EscrowCeremonyClaim(r.Context())
code := claim.Code
if err != nil {
switch status {
case http.StatusGone: // 410 — the code was minted but never shown; permanently void.
@@ -293,7 +297,63 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) {
}
return
}
if claim.ConfirmRequired {
// R-1151: the agent keeps the new package until the household types the two words back (/api/escrow/confirm);
// the ceremony is not complete yet, so nothing is stamped and no report is pushed here.
s.logger.Printf("[INFO] [web] escrow recovery code claimed (one-shot; not logged) — its package waits for the typed-back words")
escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code, "confirm_required": true, "verify_positions": claim.Positions}, "")
code, claim = "", agentapi.EscrowClaim{}
_, _ = code, claim
return
}
s.logger.Printf("[INFO] [web] escrow recovery code claimed (one-shot; not logged)")
s.escrowCompleted()
escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code}, "")
code = "" // drop the reference promptly (GC caveat: best-effort)
_ = code
}
// escrowConfirmAPIHandler (R-1151) takes the two words the household typed back and hands them to the agent, which
// stores the new package on the hub only when they match. Body: JSON {"words": [w1, w2]}. The words are part of the
// code: never logged. On success the ceremony is complete (escrowCompleted).
func (s *Server) escrowConfirmAPIHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
var req struct {
Words []string `json:"words"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil || len(req.Words) != 2 {
escrowJSON(w, http.StatusBadRequest, nil, s.msg(r, "api.escrow.confirm_words_mismatch"))
return
}
agent, err := s.escrowAgentConn()
if err != nil {
escrowJSON(w, http.StatusBadGateway, nil, "Az ügynök nem elérhető.")
return
}
status, err := agent.EscrowCeremonyConfirm(r.Context(), req.Words)
req.Words = nil
if err != nil {
switch status {
case http.StatusUnprocessableEntity, http.StatusBadRequest: // the words do not match — the wait goes on
escrowJSON(w, http.StatusUnprocessableEntity, nil, s.msg(r, "api.escrow.confirm_words_mismatch"))
case http.StatusGone:
escrowJSON(w, http.StatusGone, map[string]any{"restart": true}, s.msg(r, "api.escrow.confirm_expired"))
case http.StatusNotFound:
escrowJSON(w, http.StatusGone, map[string]any{"restart": true}, s.msg(r, "api.escrow.confirm_lost"))
default:
s.logger.Printf("[WARN] [web] escrow confirm failed (status %d)", status)
escrowJSON(w, http.StatusBadGateway, nil, s.msg(r, "api.escrow.confirm_store_failed"))
}
return
}
s.logger.Printf("[INFO] [web] escrow recovery code typed back — the new package is on the hub (R-1151)")
s.escrowCompleted()
escrowJSON(w, http.StatusOK, map[string]any{"uploaded": true}, "")
}
// escrowCompleted runs when the new package is on the hub: at the claim for an agent older than 0.160.0, at the
// typed-back confirm since (R-1151).
func (s *Server) escrowCompleted() {
// v0.138.0: stamp the ceremony-completed time so /backups/remote shows the "awaiting hub
// confirmation" card during the report-cycle gap before the auto-confirmer flips to escrowed
// (Phase-0 verdict A: the yellow "szükséges" card during that ~15-min wait was the real gap).
@@ -310,9 +370,6 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) {
// hub's ACK hash-match flip pending→escrowed in seconds (EscrowAutoConfirmer, unchanged)
// instead of after the next ~15-min cycle. Non-blocking; a failed push degrades to the cycle.
s.reportTriggerNow()
escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code}, "")
code = "" // drop the reference promptly (GC caveat: best-effort)
_ = code
}
// clearAuthFailures clears the login counter after a right re-auth password — the counter reserveLoginAttempt counts