diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 90915b13b3..74204aeb2a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -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. diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index 3590e5455c..d5f61d0659 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -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. diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index 3cf23823d2..c02e82b99c 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -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 { diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 6c53fb3d89..ca12b97752 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -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 {