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:
Kacper Sawicki
2026-01-23 13:48:27 +01:00
committed by GitHub
parent fa7baebdd8
commit 9843adb8c6
4 changed files with 46 additions and 10 deletions
+4 -4
View File
@@ -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.
+3 -3
View File
@@ -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.
+13
View File
@@ -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 {
+26 -3
View File
@@ -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 {