package store // settings_cascade.go // // Pure functions for three-tier settings resolution: // global (admin) → team → user // // Two orthogonal axes: // - RBAC controls who can write at each scope // - user_overridable (per setting key) controls whether lower scopes // can override higher scope values // // The kernel enforces the cascade without knowing setting semantics. import "encoding/json" // SettingSchema represents one entry from the manifest "settings" array. type SettingSchema struct { Key string `json:"key"` UserOverridable *bool `json:"user_overridable,omitempty"` } // IsUserOverridable returns whether a setting key can be overridden at // lower scopes (team, user). Defaults to true when the field is absent // for backward compatibility with existing manifests. func (s SettingSchema) IsUserOverridable() bool { return s.UserOverridable == nil || *s.UserOverridable } // ParseSettingsSchema extracts []SettingSchema from a package manifest. func ParseSettingsSchema(manifest map[string]any) []SettingSchema { arr, ok := manifest["settings"].([]any) if !ok { return nil } result := make([]SettingSchema, 0, len(arr)) for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } s := SettingSchema{} s.Key, _ = m["key"].(string) if s.Key == "" { continue } if uo, ok := m["user_overridable"].(bool); ok { s.UserOverridable = &uo } result = append(result, s) } return result } // ResolveSettings implements the settings cascade: // // user override → team override → global default // // For each key declared in the schema with user_overridable: false, // team and user values are ignored — only the global value is used. // Keys not declared in schema default to overridable. // // Parameters: // - globalSettings: admin-set values from packages.package_settings // - teamSettings: team-scoped overrides (nil if no team context) // - userSettings: per-user overrides (nil if no user context) // - schema: parsed manifest settings array func ResolveSettings( globalSettings json.RawMessage, teamSettings json.RawMessage, userSettings json.RawMessage, schema []SettingSchema, ) map[string]any { global := jsonToMap(globalSettings) team := jsonToMap(teamSettings) user := jsonToMap(userSettings) // Build overridable lookup from schema overridable := make(map[string]bool, len(schema)) for _, s := range schema { overridable[s.Key] = s.IsUserOverridable() } // Start with global as base merged := make(map[string]any, len(global)) for k, v := range global { merged[k] = v } // Overlay team values (respecting user_overridable) for k, v := range team { canOverride, declared := overridable[k] if !declared { canOverride = true // unknown keys default to overridable } if canOverride { merged[k] = v } } // Overlay user values (respecting user_overridable) for k, v := range user { canOverride, declared := overridable[k] if !declared { canOverride = true } if canOverride { merged[k] = v } } return merged } // FilterOverridableKeys strips non-overridable keys from a settings map. // Used by handlers to enforce user_overridable before saving. // Returns the filtered map and the list of rejected keys. func FilterOverridableKeys(settings map[string]any, schema []SettingSchema) (map[string]any, []string) { overridable := make(map[string]bool, len(schema)) for _, s := range schema { overridable[s.Key] = s.IsUserOverridable() } filtered := make(map[string]any, len(settings)) var rejected []string for k, v := range settings { canOverride, declared := overridable[k] if !declared { canOverride = true } if canOverride { filtered[k] = v } else { rejected = append(rejected, k) } } return filtered, rejected } func jsonToMap(raw json.RawMessage) map[string]any { if len(raw) == 0 { return nil } var m map[string]any json.Unmarshal(raw, &m) return m }