chore: add enterprise feature for aibridge (#19976)

Adds enterprise feature "aibridge" and gates the aibridge CRUD and LLM API endpoints behind it.
This commit is contained in:
Dean Sheather
2025-09-27 01:13:06 +10:00
committed by GitHub
parent 65f2895c0d
commit 43415f0144
7 changed files with 86 additions and 1 deletions
+4
View File
@@ -90,6 +90,7 @@ const (
// enterprise/coderd/license/license.go for the license format.
FeatureManagedAgentLimit FeatureName = "managed_agent_limit"
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
FeatureAIBridge FeatureName = "aibridge"
)
var (
@@ -117,6 +118,7 @@ var (
FeatureWorkspacePrebuilds,
FeatureManagedAgentLimit,
FeatureWorkspaceExternalAgent,
FeatureAIBridge,
}
// FeatureNamesMap is a map of all feature names for quick lookups.
@@ -136,6 +138,8 @@ func (n FeatureName) Humanize() string {
return "Template RBAC"
case FeatureSCIM:
return "SCIM"
case FeatureAIBridge:
return "AI Bridge"
default:
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
}
+16
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
@@ -31,6 +32,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
now := dbtime.Now()
@@ -77,6 +83,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -158,6 +169,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
+5 -1
View File
@@ -148,8 +148,12 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
experiments := agplcoderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value())
var aibridgeDaemon *aibridged.Server
// In-memory aibridge daemon.
// TODO(@deansheather): the lifecycle of the aibridged server is
// probably better managed by the enterprise API type itself. Managing
// it in the API type means we can avoid starting it up when the license
// is not entitled to the feature.
var aibridgeDaemon *aibridged.Server
if options.DeploymentValues.AI.BridgeConfig.Enabled {
if experiments.Enabled(codersdk.ExperimentAIBridge) {
aibridgeDaemon, err = newAIBridgeDaemon(api)
+56
View File
@@ -1,6 +1,7 @@
package coderd_test
import (
"net/http"
"testing"
"time"
@@ -15,12 +16,37 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
func TestAIBridgeListInterceptions(t *testing.T) {
t.Parallel()
t.Run("RequiresLicenseFeature", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentAIBridge)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
// No aibridge feature
Features: license.Features{},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
ctx := testutil.Context(t, testutil.WaitLong)
_, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
require.Equal(t, "AI Bridge is a Premium feature. Contact sales!", sdkErr.Message)
})
t.Run("EmptyDB", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
@@ -29,6 +55,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -45,6 +76,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -126,6 +162,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -210,6 +251,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
adminExperimentalClient := codersdk.NewExperimentalClient(adminClient)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -249,6 +295,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
_, secondUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
@@ -407,6 +458,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
experimentalClient := codersdk.NewExperimentalClient(client)
+2
View File
@@ -229,6 +229,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
api.AGPL.ExperimentalHandler.Group(func(r chi.Router) {
r.Route("/aibridge", func(r chi.Router) {
r.Use(
api.RequireFeatureMW(codersdk.FeatureAIBridge),
httpmw.RequireExperimentWithDevBypass(api.AGPL.Experiments, codersdk.ExperimentAIBridge),
)
r.Group(func(r chi.Router) {
@@ -770,6 +771,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
codersdk.FeatureUserRoleManagement: true,
codersdk.FeatureAccessControl: true,
codersdk.FeatureControlSharedPorts: true,
codersdk.FeatureAIBridge: true,
})
if err != nil {
return codersdk.Entitlements{}, err
@@ -890,6 +890,7 @@ func TestLicenseEntitlements(t *testing.T) {
codersdk.FeatureAccessControl: true,
codersdk.FeatureControlSharedPorts: true,
codersdk.FeatureWorkspaceExternalAgent: true,
codersdk.FeatureAIBridge: true,
}
legacyLicense := func() *coderdenttest.LicenseOptions {
+2
View File
@@ -1222,6 +1222,7 @@ export interface Feature {
// From codersdk/deployment.go
export type FeatureName =
| "aibridge"
| "access_control"
| "advanced_template_scheduling"
| "appearance"
@@ -1246,6 +1247,7 @@ export type FeatureName =
| "workspace_proxy";
export const FeatureNames: FeatureName[] = [
"aibridge",
"access_control",
"advanced_template_scheduling",
"appearance",