package store import ( "strings" "gitea.dooplex.hu/admin/felhom-hub/internal/semver" ) // ── The MinAgent a floor DECLARES (hub v0.112.0, R-472) ─────────────────────────────────────────── // // Publish-train rule 1 said "the manifest leads the floor": a floor above the vouched golden was HELD, // because the only record of a controller's agent requirement was the golden manifest's MinAgent, and // that value describes the golden, not the release being served (R-216). Operator ruling 2026-09-13 // (option B): a floor may carry its own requirement, read from the release's CHANGELOG header // (`**MinAgent: X.Y.Z**`, enforced by the controller's gate). With it, the same agent comparison applies // above the golden; without it, the hold is exactly as before. // // STORED AS "FLOOR=MINAGENT", ON PURPOSE. A declaration describes ONE release. Storing the MinAgent // alone would let a later floor raise — made without re-declaring — silently inherit the old // requirement, which is a stale fact wearing a current label. So a declaration counts only while the // floor it was made for is the floor in force; any other floor reads as undeclared and is held above // the golden. Pinned by TestDeclaredMinAgent_DoesNotCarryToADifferentFloor. const settingGlobalFloorDeclaredMinAgent = "min_controller_version_declared_min_agent" func encodeDeclaredMinAgent(floor, minAgent string) string { if floor == "" || minAgent == "" { return "" } return floor + "=" + minAgent } // decodeDeclaredMinAgent returns the declared MinAgent only if it was declared for `floor` and parses. func decodeDeclaredMinAgent(stored, floor string) string { parts := strings.SplitN(stored, "=", 2) if len(parts) != 2 || floor == "" || parts[0] != floor || !semver.Valid(parts[1]) { return "" } return parts[1] } // SetGlobalFloorDeclaredMinAgent records the MinAgent declared with the global floor (empty clears). func (s *Store) SetGlobalFloorDeclaredMinAgent(floor, minAgent string) error { return s.setSetting(settingGlobalFloorDeclaredMinAgent, encodeDeclaredMinAgent(floor, minAgent)) } // GlobalFloorDeclaredMinAgent returns the MinAgent declared for the CURRENT global floor, or "". func (s *Store) GlobalFloorDeclaredMinAgent() string { return decodeDeclaredMinAgent(s.getSetting(settingGlobalFloorDeclaredMinAgent), s.GetGlobalMinControllerVersion()) } // SetCustomerFloorDeclaredMinAgent records the MinAgent declared with a per-customer floor. func (s *Store) SetCustomerFloorDeclaredMinAgent(customerID, floor, minAgent string) error { _, err := s.db.Exec(` UPDATE customer_configs SET min_controller_declared_min_agent = ?, updated_at = datetime('now') WHERE customer_id = ?`, encodeDeclaredMinAgent(floor, minAgent), customerID, ) return err } // CustomerFloorDeclaredMinAgent returns the MinAgent declared for the customer's CURRENT override, or "". func (s *Store) CustomerFloorDeclaredMinAgent(customerID string) string { var stored, floor string if err := s.db.QueryRow(`SELECT min_controller_declared_min_agent, min_controller_version FROM customer_configs WHERE customer_id = ?`, customerID).Scan(&stored, &floor); err != nil { return "" } return decodeDeclaredMinAgent(stored, floor) } // DeclaredFloorMinAgent returns the MinAgent declared with the floor that WINS for this customer — // the per-customer override's own declaration when an override is in force, else the global floor's. // The same precedence as EffectiveMinControllerVersion, so a declaration never attaches to a floor it // was not made for. func (s *Store) DeclaredFloorMinAgent(customerID string) string { if cfg, err := s.GetCustomerConfig(customerID); err == nil && cfg != nil && cfg.MinControllerVersion != "" { return s.CustomerFloorDeclaredMinAgent(customerID) } return s.GlobalFloorDeclaredMinAgent() }