Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4e3bd98
fix: remove wording from `<ManagedAgentsConsumption />`
jakehwll Feb 9, 2026
de6e3be
fix: update messaging in `Warnings`
jakehwll Feb 9, 2026
063c5fb
fix: remove limiting factor
jakehwll Feb 9, 2026
0eff333
fix: lint + comments
jakehwll Feb 9, 2026
e22dc20
fix: remove `limit` from `<ManagedAgentsConsumption />`
jakehwll Feb 9, 2026
8cae669
feat: ux improvement on `<ManagedAgentsConsumption />`
jakehwll Feb 9, 2026
5df4c07
fix: remove `Please contact sales or refer to the Deployment Licenses…
jakehwll Feb 9, 2026
ead69cd
fix: update frontend to remove limit from view
jakehwll Feb 9, 2026
956d506
Merge branch 'main' into jakehwll/1281-remove-agent-workspaces-limit
jakehwll Feb 11, 2026
7cb2dd4
fix: trailing space removal
jakehwll Feb 11, 2026
2f52ecd
fix: de-mui `<LicenseBannerView />`
jakehwll Feb 11, 2026
666e49f
feat: link `LicenseManagedAgentLimitExceededErrorText` to `docs("/ai-…
jakehwll Feb 11, 2026
059416f
fix: remove usage of limit
jakehwll Feb 11, 2026
25e713e
fix: restore `license_test.go` test
jakehwll Feb 11, 2026
a89ee5e
fix: resolve order of `<LicenseBannerView />`
jakehwll Feb 11, 2026
63a5d87
feat: add `ManagedAgentLimitExceeded` stories
jakehwll Feb 11, 2026
a4e0de2
fix: add `messageLink` helper and multi-message linking
jakehwll Feb 11, 2026
c0412a1
fix: remove duplicate link
jakehwll Feb 11, 2026
7b1113a
fix: allow `messageLink` to define a `target`
jakehwll Feb 11, 2026
732e809
chore: rename to `messageLinkProps`
jakehwll Feb 16, 2026
d2c78bc
fix: resolve react imports
jakehwll Feb 16, 2026
f951cbf
fix: rename to `LicenseManagedAgentLimitExceededWarningText`
jakehwll Feb 16, 2026
42de4da
fix: remove unused function
jakehwll Feb 16, 2026
5876f1a
Merge branch 'main' into jakehwll/1281-remove-agent-workspaces-limit
jakehwll Feb 16, 2026
e94d896
Merge branch 'main' into jakehwll/1281-remove-agent-workspaces-limit
jakehwll Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions codersdk/licenses.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import (
)

const (
LicenseExpiryClaim = "license_expires"
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
LicenseExpiryClaim = "license_expires"
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
LicenseManagedAgentLimitExceededWarningText = "You have built more workspaces with managed agents than your license allows."
)

type AddLicenseRequest struct {
Expand Down
63 changes: 7 additions & 56 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,13 @@ func (api *API) updateEntitlements(ctx context.Context) error {

var _ wsbuilder.UsageChecker = &API{}

func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion, task *database.Task, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
func (api *API) CheckBuildUsage(
_ context.Context,
_ database.Store,
templateVersion *database.TemplateVersion,
_ *database.Task,
_ database.WorkspaceTransition,
) (wsbuilder.UsageCheckResponse, error) {
// If the template version has an external agent, we need to check that the
// license is entitled to this feature.
if templateVersion.HasExternalAgent.Valid && templateVersion.HasExternalAgent.Bool {
Expand All @@ -996,61 +1002,6 @@ func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templ
}
}

resp, err := api.checkAIBuildUsage(ctx, store, task, transition)
if err != nil {
return wsbuilder.UsageCheckResponse{}, err
}
if !resp.Permitted {
return resp, nil
}

return wsbuilder.UsageCheckResponse{Permitted: true}, nil
}

// checkAIBuildUsage validates AI-related usage constraints. It is a no-op
// unless the transition is "start" and the template version has an AI task.
func (api *API) checkAIBuildUsage(ctx context.Context, store database.Store, task *database.Task, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
// Only check AI usage rules for start transitions.
if transition != database.WorkspaceTransitionStart {
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
}

// If the template version doesn't have an AI task, we don't need to check usage.
if task == nil {
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
}

// When licensed, ensure we haven't breached the managed agent limit.
// Unlicensed deployments are allowed to use unlimited managed agents.
if api.Entitlements.HasLicense() {
managedAgentLimit, ok := api.Entitlements.Feature(codersdk.FeatureManagedAgentLimit)
if !ok || !managedAgentLimit.Enabled || managedAgentLimit.Limit == nil || managedAgentLimit.UsagePeriod == nil {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "Your license is not entitled to managed agents. Please contact sales to continue using managed agents.",
}, nil
}

// This check is intentionally not committed to the database. It's fine
// if it's not 100% accurate or allows for minor breaches due to build
// races.
// nolint:gocritic // Requires permission to read all usage events.
managedAgentCount, err := store.GetTotalUsageDCManagedAgentsV1(agpldbauthz.AsSystemRestricted(ctx), database.GetTotalUsageDCManagedAgentsV1Params{
StartDate: managedAgentLimit.UsagePeriod.Start,
EndDate: managedAgentLimit.UsagePeriod.End,
})
if err != nil {
return wsbuilder.UsageCheckResponse{}, xerrors.Errorf("get managed agent count: %w", err)
}

if managedAgentCount >= *managedAgentLimit.Limit {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "You have breached the managed agent limit in your license. Please contact sales to continue using managed agents.",
}, nil
}
}

return wsbuilder.UsageCheckResponse{Permitted: true}, nil
}

Expand Down
32 changes: 17 additions & 15 deletions enterprise/coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,15 +765,20 @@ func TestManagedAgentLimit(t *testing.T) {
require.NoError(t, err, "fetching AI workspace must succeed")
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace.LatestBuild.ID)

// Create a second AI task, which should fail due to breaching the limit.
_, err = cli.CreateTask(ctx, owner.UserID.String(), codersdk.CreateTaskRequest{
// Create a second AI task, which should succeed even though the limit is
// breached. Managed agent limits are advisory only and should never block
// workspace creation.
task2, err := cli.CreateTask(ctx, owner.UserID.String(), codersdk.CreateTaskRequest{
Name: namesgenerator.UniqueNameWith("-"),
TemplateVersionID: aiTemplate.ActiveVersionID,
TemplateVersionPresetID: uuid.Nil,
Input: "hi",
DisplayName: namesgenerator.UniqueName(),
})
require.ErrorContains(t, err, "You have breached the managed agent limit in your license")
require.NoError(t, err, "creating task beyond managed agent limit must succeed")
workspace2, err := cli.Workspace(ctx, task2.WorkspaceID.UUID)
require.NoError(t, err, "fetching AI workspace must succeed")
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace2.LatestBuild.ID)

// Create a third workspace using the same template, which should succeed.
workspace = coderdtest.CreateWorkspace(t, cli, aiTemplate.ID)
Expand All @@ -784,12 +789,12 @@ func TestManagedAgentLimit(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace.LatestBuild.ID)
}

func TestCheckBuildUsage_SkipsAIForNonStartTransitions(t *testing.T) {
func TestCheckBuildUsage_NeverBlocksOnManagedAgentLimit(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Prepare entitlements with a managed agent limit to enforce.
// Prepare entitlements with a managed agent limit.
entSet := entitlements.New()
entSet.Modify(func(e *codersdk.Entitlements) {
e.HasLicense = true
Expand Down Expand Up @@ -825,27 +830,24 @@ func TestCheckBuildUsage_SkipsAIForNonStartTransitions(t *testing.T) {
TemplateVersionID: tv.ID,
}

// Mock DB: expect exactly one count call for the "start" transition.
// Mock DB: no calls expected since managed agent limits are
// advisory only and no longer query the database at build time.
mDB := dbmock.NewMockStore(ctrl)
mDB.EXPECT().
GetTotalUsageDCManagedAgentsV1(gomock.Any(), gomock.Any()).
Times(1).
Return(int64(1), nil) // equal to limit -> should breach

ctx := context.Background()

// Start transition: should be not permitted due to limit breach.
// Start transition: should be permitted even though the limit is
// breached. Managed agent limits are advisory only.
startResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStart)
require.NoError(t, err)
require.False(t, startResp.Permitted)
require.Contains(t, startResp.Message, "breached the managed agent limit")
require.True(t, startResp.Permitted)

// Stop transition: should be permitted and must not trigger additional DB calls.
// Stop transition: should also be permitted.
stopResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStop)
require.NoError(t, err)
require.True(t, stopResp.Permitted)

// Delete transition: should be permitted and must not trigger additional DB calls.
// Delete transition: should also be permitted.
deleteResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionDelete)
require.NoError(t, err)
require.True(t, deleteResp.Permitted)
Expand Down
2 changes: 1 addition & 1 deletion enterprise/coderd/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ func LicensesEntitlements(
}
if managedAgentCount >= *agentLimit.Limit {
entitlements.Warnings = append(entitlements.Warnings,
"You have built more workspaces with managed agents than your license allows. Further managed agent builds will be blocked.")
codersdk.LicenseManagedAgentLimitExceededWarningText)
} else if managedAgentCount >= softWarningThreshold {
entitlements.Warnings = append(entitlements.Warnings,
"You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.")
Expand Down
2 changes: 1 addition & 1 deletion enterprise/coderd/license/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ func TestLicenseEntitlements(t *testing.T) {
},
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assert.Len(t, entitlements.Warnings, 1)
assert.Equal(t, "You have built more workspaces with managed agents than your license allows. Further managed agent builds will be blocked.", entitlements.Warnings[0])
assert.Equal(t, "You have built more workspaces with managed agents than your license allows.", entitlements.Warnings[0])
assertNoErrors(t, entitlements)

feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
Expand Down
4 changes: 4 additions & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions site/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
--radius: 0.5rem;
--highlight-purple: 271, 61%, 35%;
--highlight-green: 143 64% 24%;
--highlight-orange: 30 100% 54%;
--highlight-grey: 240 5% 65%;
--highlight-sky: 195, 61%, 22%;
--highlight-red: 0 74% 42%;
Expand Down Expand Up @@ -91,6 +92,7 @@
--overlay-default: 240 10% 4% / 80%;
--highlight-purple: 269, 100%, 74%;
--highlight-green: 141 79% 85%;
--highlight-orange: 31 100% 70%;
--highlight-grey: 240 4% 46%;
--highlight-sky: 188, 75%, 80%;
--highlight-red: 0 91% 71%;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { chromatic } from "testHelpers/chromatic";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { LicenseManagedAgentLimitExceededWarningText } from "api/typesGenerated";
import { LicenseBannerView } from "./LicenseBannerView";

const meta: Meta<typeof LicenseBannerView> = {
Expand Down Expand Up @@ -36,3 +37,20 @@ export const OneError: Story = {
warnings: [],
},
};

export const ManagedAgentLimitExceeded: Story = {
args: {
errors: [],
warnings: [LicenseManagedAgentLimitExceededWarningText],
},
};

export const ManagedAgentLimitExceededWithOtherWarnings: Story = {
args: {
errors: [],
warnings: [
LicenseManagedAgentLimitExceededWarningText,
"You have exceeded the number of seats in your license.",
],
},
};
Loading
Loading