@@ -8,17 +8,22 @@ import (
88 "sync"
99)
1010
11+ const maxFeatureRuleFlags = 16
12+
1113// FeatureFlag identifies a feature consistently across inventory consumers.
1214type FeatureFlag string
1315
14- // FeatureFlagChecker resolves one feature flag for the current request.
16+ // FeatureFlagChecker resolves one feature flag for the current request. Every
17+ // context value needed for availability checks must be installed before the
18+ // inventory is resolved. Handler-only checks receive the live tool-call context.
1519type FeatureFlagChecker func (ctx context.Context , flag FeatureFlag ) (bool , error )
1620
1721// FeatureResolver returns the resolved value of a feature flag.
1822// Implementations absorb resolution errors and fail closed.
1923type FeatureResolver func (flag FeatureFlag ) bool
2024
21- // FeaturePredicate determines whether an inventory item is available.
25+ // FeaturePredicate determines whether an inventory item is available. Predicates
26+ // must be pure: their result may depend only on calls to the supplied resolver.
2227type FeaturePredicate func (featureAsBool FeatureResolver ) bool
2328
2429// FeatureRule declares the feature flags used by an availability predicate.
@@ -44,11 +49,33 @@ func NewFeatureRule(features []FeatureFlag, predicate FeaturePredicate) FeatureR
4449 featureSet [feature ] = struct {}{}
4550 declared = append (declared , feature )
4651 }
47- return FeatureRule {
52+ rule := FeatureRule {
4853 features : declared ,
4954 featureSet : featureSet ,
5055 predicate : predicate ,
5156 }
57+ rule .validate ()
58+ return rule
59+ }
60+
61+ func (r FeatureRule ) validate () {
62+ if r .predicate == nil {
63+ return
64+ }
65+ if len (r .features ) > maxFeatureRuleFlags {
66+ panic (fmt .Sprintf ("feature rule declares %d flags; maximum is %d" , len (r .features ), maxFeatureRuleFlags ))
67+ }
68+
69+ for assignment := range 1 << len (r .features ) {
70+ r .evaluate (func (feature FeatureFlag ) bool {
71+ for i , declared := range r .features {
72+ if feature == declared {
73+ return assignment & (1 << i ) != 0
74+ }
75+ }
76+ return false
77+ })
78+ }
5279}
5380
5481// Features returns the feature flags referenced by the rule.
@@ -69,7 +96,10 @@ func (r FeatureRule) Enabled(featureAsBool FeatureResolver) bool {
6996 if featureAsBool == nil {
7097 return false
7198 }
99+ return r .evaluate (featureAsBool )
100+ }
72101
102+ func (r FeatureRule ) evaluate (featureAsBool FeatureResolver ) bool {
73103 var undeclared FeatureFlag
74104 usedUndeclared := false
75105 enabled := r .predicate (func (feature FeatureFlag ) bool {
@@ -81,25 +111,35 @@ func (r FeatureRule) Enabled(featureAsBool FeatureResolver) bool {
81111 return featureAsBool (feature )
82112 })
83113 if usedUndeclared {
84- fmt .Fprintf (os .Stderr , "Feature rule used undeclared feature %q\n " , undeclared )
85- return false
114+ panic (fmt .Sprintf ("feature rule used undeclared feature %q" , undeclared ))
86115 }
87116 return enabled
88117}
89118
90119type featureStateContextKey struct {}
120+ type resolvingFeatureContextKey struct {}
91121
92122type featureState struct {
93123 checker FeatureFlagChecker
94124
95- mu sync.Mutex
96- values map [FeatureFlag ]bool
125+ mu sync.Mutex
126+ results map [FeatureFlag ]* featureResult
127+ }
128+
129+ type featureResult struct {
130+ ready chan struct {}
131+ enabled bool
132+ }
133+
134+ type resolvingFeature struct {
135+ flag FeatureFlag
136+ parent * resolvingFeature
97137}
98138
99139func newFeatureState (checker FeatureFlagChecker ) * featureState {
100140 return & featureState {
101141 checker : checker ,
102- values : make (map [FeatureFlag ]bool ),
142+ results : make (map [FeatureFlag ]* featureResult ),
103143 }
104144}
105145
@@ -108,24 +148,60 @@ func (s *featureState) enabled(ctx context.Context, feature FeatureFlag) bool {
108148 return false
109149 }
110150
111- s .mu .Lock ()
112- defer s .mu .Unlock ()
151+ for current := resolvingFeatureFromContext (ctx ); current != nil ; current = current .parent {
152+ if current .flag == feature {
153+ fmt .Fprintf (os .Stderr , "Feature flag resolution cycle detected for %q\n " , feature )
154+ return false
155+ }
156+ }
113157
114- if enabled , ok := s .values [feature ]; ok {
115- return enabled
158+ s .mu .Lock ()
159+ result , found := s .results [feature ]
160+ if ! found {
161+ result = & featureResult {ready : make (chan struct {})}
162+ s .results [feature ] = result
163+ }
164+ s .mu .Unlock ()
165+
166+ if found {
167+ select {
168+ case <- result .ready :
169+ return result .enabled
170+ case <- ctx .Done ():
171+ return false
172+ }
116173 }
117174
118- enabled , err := s .checker (ctx , feature )
175+ completed := false
176+ defer func () {
177+ if ! completed {
178+ close (result .ready )
179+ }
180+ }()
181+
182+ resolutionCtx := context .WithValue (ctx , resolvingFeatureContextKey {}, & resolvingFeature {
183+ flag : feature ,
184+ parent : resolvingFeatureFromContext (ctx ),
185+ })
186+ enabled , err := s .checker (resolutionCtx , feature )
119187 if err != nil {
120188 fmt .Fprintf (os .Stderr , "Feature flag check error for %q: %v\n " , feature , err )
121189 enabled = false
122190 }
123- s .values [feature ] = enabled
191+ result .enabled = enabled
192+ completed = true
193+ close (result .ready )
124194 return enabled
125195}
126196
197+ func resolvingFeatureFromContext (ctx context.Context ) * resolvingFeature {
198+ feature , _ := ctx .Value (resolvingFeatureContextKey {}).(* resolvingFeature )
199+ return feature
200+ }
201+
127202// WithResolvedFeatures resolves the deduplicated feature names into state owned
128- // by the returned context. Repeated calls extend and reuse that state.
203+ // by the returned context. Repeated calls extend and reuse that state. When state
204+ // already exists, its checker is authoritative and checker is ignored.
129205func WithResolvedFeatures (ctx context.Context , checker FeatureFlagChecker , features []FeatureFlag ) context.Context {
130206 state , _ := ctx .Value (featureStateContextKey {}).(* featureState )
131207 if state == nil {
@@ -145,18 +221,20 @@ func WithResolvedFeatures(ctx context.Context, checker FeatureFlagChecker, featu
145221}
146222
147223// ResolveFeature returns a feature value from request-owned resolution state.
148- // Features not resolved up front are resolved lazily and cached.
149- func ResolveFeature (ctx context.Context , checker FeatureFlagChecker , feature FeatureFlag ) bool {
224+ // Context state and its checker are authoritative. fallbackChecker is used only
225+ // when the context has no state; that uncached compatibility path lets handlers
226+ // invoked directly outside a server continue to resolve features.
227+ func ResolveFeature (ctx context.Context , fallbackChecker FeatureFlagChecker , feature FeatureFlag ) bool {
150228 if feature == "" {
151229 return false
152230 }
153231 if state , _ := ctx .Value (featureStateContextKey {}).(* featureState ); state != nil {
154232 return state .enabled (ctx , feature )
155233 }
156- if checker == nil {
234+ if fallbackChecker == nil {
157235 return false
158236 }
159- return newFeatureState (checker ).enabled (ctx , feature )
237+ return newFeatureState (fallbackChecker ).enabled (ctx , feature )
160238}
161239
162240func featureResolver (ctx context.Context , checker FeatureFlagChecker ) FeatureResolver {
@@ -173,19 +251,3 @@ func featureResolver(ctx context.Context, checker FeatureFlagChecker) FeatureRes
173251 return state .enabled (ctx , feature )
174252 }
175253}
176-
177- func collectFeatures (rules ... FeatureRule ) []FeatureFlag {
178- seen := make (map [FeatureFlag ]struct {})
179- for _ , rule := range rules {
180- for _ , feature := range rule .features {
181- seen [feature ] = struct {}{}
182- }
183- }
184-
185- features := make ([]FeatureFlag , 0 , len (seen ))
186- for feature := range seen {
187- features = append (features , feature )
188- }
189- slices .Sort (features )
190- return features
191- }
0 commit comments