mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(codersdk): remove AI Bridge entitlement from Premium license (#21540)
## Summary AI Bridge is moving out of Premium as a separate add-on (GA in Feb 3). Closes https://github.com/coder/internal/issues/1226 ## Changes - Excludes `FeatureAIBridge` from `Enterprise()` and `FeatureSetPremium.Features()` - Adds soft warning for deployments with AI Bridge enabled but not entitled - Warning is displayed to Auditor/Owner roles in UI banner and CLI headers ## Warning Message When AI Bridge is enabled (`CODER_AIBRIDGE_ENABLED=true`) but the license doesn't include the entitlement: > AI Bridge has reached General Availability and your Coder deployment is not entitled to run this feature. Contact your account team (https://coder.com/contact) for information around getting a license with AI Bridge. ## Behavior - The feature remains usable in v2.30 (soft warning only) - Future versions may include hard enforcement
This commit is contained in:
@@ -170,7 +170,8 @@ func (n FeatureName) AlwaysEnable() bool {
|
||||
func (n FeatureName) Enterprise() bool {
|
||||
switch n {
|
||||
// Add all features that should be excluded in the Enterprise feature set.
|
||||
case FeatureMultipleOrganizations, FeatureCustomRoles:
|
||||
// AI Bridge is a separate add-on.
|
||||
case FeatureMultipleOrganizations, FeatureCustomRoles, FeatureAIBridge:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
@@ -224,11 +225,10 @@ func (set FeatureSet) Features() []FeatureName {
|
||||
case FeatureSetPremium:
|
||||
premiumFeatures := make([]FeatureName, len(FeatureNames))
|
||||
copy(premiumFeatures, FeatureNames)
|
||||
// Remove the selection
|
||||
// Remove features that use limits and AI Bridge (now a separate add-on).
|
||||
premiumFeatures = slices.DeleteFunc(premiumFeatures, func(f FeatureName) bool {
|
||||
return f.UsesLimit()
|
||||
return f.UsesLimit() || f == FeatureAIBridge
|
||||
})
|
||||
// FeatureSetPremium is just all features.
|
||||
return premiumFeatures
|
||||
}
|
||||
// By default, return an empty set.
|
||||
|
||||
@@ -623,16 +623,16 @@ func TestPremiumSuperSet(t *testing.T) {
|
||||
// Premium ⊃ Enterprise
|
||||
require.Subset(t, premium.Features(), enterprise.Features(), "premium should be a superset of enterprise. If this fails, update the premium feature set to include all enterprise features.")
|
||||
|
||||
// Premium = All Features EXCEPT usage limit features
|
||||
// Premium = All Features EXCEPT usage limit features and AI Bridge (add-on).
|
||||
expectedPremiumFeatures := []codersdk.FeatureName{}
|
||||
for _, feature := range codersdk.FeatureNames {
|
||||
if feature.UsesLimit() {
|
||||
if feature.UsesLimit() || feature == codersdk.FeatureAIBridge {
|
||||
continue
|
||||
}
|
||||
expectedPremiumFeatures = append(expectedPremiumFeatures, feature)
|
||||
}
|
||||
require.NotEmpty(t, expectedPremiumFeatures, "expectedPremiumFeatures should not be empty")
|
||||
require.ElementsMatch(t, premium.Features(), expectedPremiumFeatures, "premium should contain all features except usage limit features")
|
||||
require.ElementsMatch(t, premium.Features(), expectedPremiumFeatures, "premium should contain all features except usage limit features and AI Bridge")
|
||||
|
||||
// This check exists because if you misuse the slices.Delete, you can end up
|
||||
// with zero'd values.
|
||||
|
||||
@@ -548,6 +548,15 @@ func LicensesEntitlements(
|
||||
featureArguments.ActiveUserCount, *userLimit.Limit))
|
||||
}
|
||||
|
||||
// AI Bridge has a custom warning message as it moved to a separate add-on.
|
||||
aiBridge := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
if aiBridge.Enabled && aiBridge.Entitlement != codersdk.EntitlementEntitled {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"AI Bridge has reached General Availability and your Coder deployment is not entitled "+
|
||||
"to run this feature. Contact your account team (https://coder.com/contact) for "+
|
||||
"information around getting a license with AI Bridge.")
|
||||
}
|
||||
|
||||
// Add a warning for every feature that is enabled but not entitled or
|
||||
// is in a grace period.
|
||||
for _, featureName := range codersdk.FeatureNames {
|
||||
@@ -567,6 +576,10 @@ func LicensesEntitlements(
|
||||
if featureName == codersdk.FeatureManagedAgentLimit {
|
||||
continue
|
||||
}
|
||||
// AI Bridge has a custom warning message as it moved to a separate add-on.
|
||||
if featureName == codersdk.FeatureAIBridge {
|
||||
continue
|
||||
}
|
||||
|
||||
feature := entitlements.Features[featureName]
|
||||
if !feature.Enabled {
|
||||
|
||||
@@ -25,6 +25,10 @@ func TestEntitlements(t *testing.T) {
|
||||
t.Parallel()
|
||||
all := make(map[codersdk.FeatureName]bool)
|
||||
for _, n := range codersdk.FeatureNames {
|
||||
// AI Bridge is a separate add-on, not included in standard feature sets.
|
||||
if n == codersdk.FeatureAIBridge {
|
||||
continue
|
||||
}
|
||||
all[n] = true
|
||||
}
|
||||
|
||||
@@ -366,9 +370,9 @@ func TestEntitlements(t *testing.T) {
|
||||
require.True(t, entitlements.HasLicense)
|
||||
require.False(t, entitlements.Trial)
|
||||
for _, featureName := range codersdk.FeatureNames {
|
||||
if featureName == codersdk.FeatureUserLimit || featureName == codersdk.FeatureHighAvailability || featureName == codersdk.FeatureMultipleExternalAuth || featureName == codersdk.FeatureManagedAgentLimit {
|
||||
if featureName == codersdk.FeatureUserLimit || featureName == codersdk.FeatureHighAvailability || featureName == codersdk.FeatureMultipleExternalAuth || featureName == codersdk.FeatureManagedAgentLimit || featureName == codersdk.FeatureAIBridge {
|
||||
// These fields don't generate warnings when not entitled unless
|
||||
// a limit is breached.
|
||||
// a limit is breached, or have custom warning messages.
|
||||
continue
|
||||
}
|
||||
niceName := featureName.Humanize()
|
||||
@@ -798,6 +802,25 @@ func TestEntitlements(t *testing.T) {
|
||||
require.Equal(t, "You have multiple External Auth Providers configured but your license is expired. Reduce to one.", entitlements.Warnings[0])
|
||||
})
|
||||
|
||||
t.Run("AIBridgeEnabledNotEntitled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAuditLog: 1, // Some feature but not AI Bridge.
|
||||
},
|
||||
}),
|
||||
})
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAIBridge: true, // Enabled but not entitled.
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
require.Contains(t, entitlements.Warnings, "AI Bridge has reached General Availability and your Coder deployment is not entitled to run this feature. Contact your account team (https://coder.com/contact) for information around getting a license with AI Bridge.")
|
||||
})
|
||||
|
||||
t.Run("ManagedAgentLimitHasValue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -892,7 +915,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
codersdk.FeatureAccessControl: true,
|
||||
codersdk.FeatureControlSharedPorts: true,
|
||||
codersdk.FeatureWorkspaceExternalAgent: true,
|
||||
codersdk.FeatureAIBridge: true,
|
||||
// FeatureAIBridge is intentionally not included as it's a separate add-on.
|
||||
}
|
||||
|
||||
legacyLicense := func() *coderdenttest.LicenseOptions {
|
||||
|
||||
Reference in New Issue
Block a user