chore: remove agents experiment flag and mark feature as beta (#24432)

Remove the `ExperimentAgents` feature flag so the Agents feature is
always available without requiring `--experiments=agents`. The feature
is now in beta.

Existing deployments that still pass `--experiments=agents` will get a
harmless "ignoring unknown experiment" warning on startup.

### Changes

**Backend:**
- Remove `RequireExperimentWithDevBypass` middleware from chat and MCP
server routes
- Always include `AgentsAccessRole` in assignable site roles (later
refactored to org-scoped on main; rebase keeps that)
- Always set `AgentsTabVisible = true`, then drop the entire dead
`AgentsTabVisible` metadata pipeline (Go htmlState field,
populateHTMLState goroutine, HTML meta tag, useEmbeddedMetadata
registration, mock); no production consumer reads it. `AgentsNavItem`
already gates on `permissions.createChat`.
- Make `blob:` CSP `img-src` addition unconditional
- Remove `ExperimentAgents` constant, `DisplayName` case, and
`ExperimentsKnown` entry

**CLI:**
- Graduate the agents TUI from `coder exp agents` to `coder agents`
(moved from `AGPLExperimental()` to `CoreSubcommands()`)
- Drop the `agent` alias so it does not collide with the hidden
workspace-agent command
- Rename implementation files `cli/exp_agents_*.go` -> `cli/agents_*.go`
and internal identifiers (`expChatsTUIModel` -> `chatsTUIModel`,
`newExpChatsTUIModel` -> `newChatsTUIModel`, `setupExpAgentsBackend` ->
`setupAgentsBackend`, `startExpAgentsSession` -> `startAgentsSession`,
`expAgentsPtr` -> `agentsPtr`, `expAgentsSession` -> `agentsSession`,
`TestExpAgents*` -> `TestAgents*`). `expClient` (the
`*codersdk.ExperimentalClient` local) is kept; `coderd/exp_chats*.go`
and other still-experimental `cli/exp_*.go` commands are intentionally
untouched.

**Frontend:**
- Remove experiment check from `AgentsNavItem` - render when
`canCreateChat` is true
- Remove `agentsEnabled` experiment check from `WorkspacesPage`, then
gate `chatsByWorkspace` on `permissions.createChat` so users without
chat access don't trigger the per-page DB query (Copilot review
feedback)
- Add `FeatureStageBadge` (beta) next to the Coder logo in the Agents
sidebar (desktop + mobile)

**Docs:**
- Remove experiment flag setup instructions from `early-access.md` and
`getting-started.md` (and rename `early-access.md`'s "Enable Coder
Agents" heading to "Set up Coder Agents", since there is no enablement
step left)
- Update `chats-api.md` and `getting-started.md`'s Chats API note to say
"beta" instead of "experimental"
- `docs/manifest.json`: drop "experimental" from the Chats API sidebar
description
- `make gen` regenerated `docs/reference/cli/agents.md` and the CLI
index
- `scripts/check_emdash.sh`: exclude `cli/testdata/*.golden` and
`enterprise/cli/testdata/*.golden` from the new repo-wide emdash lint,
since serpent emits emdash borders in every generated `--help` golden
file

**Tests:**
- Remove `ExperimentAgents` setup from all test files (14 occurrences
across 7 files)
- Update stale "with the agents experiment" comments in
`coderd/x/chatd/integration_test.go` and `coderd/mcp_test.go`


<img width="1185" height="900" alt="image"
src="https://github.com/user-attachments/assets/b420bc8f-41d6-42c6-abd8-ad572533d651"
/>


> 🤖 Generated by Coder Agents
This commit is contained in:
Dean Sheather
2026-05-01 01:49:00 +10:00
committed by GitHub
parent b975262a97
commit e57525002c
48 changed files with 214 additions and 265 deletions
-4
View File
@@ -16271,12 +16271,10 @@ const docTemplate = `{
"notifications",
"workspace-usage",
"oauth2",
"agents",
"mcp-server-http",
"workspace-build-updates"
],
"x-enum-comments": {
"ExperimentAgents": "Enables agent-powered chat functionality.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentExample": "This isn't used for anything.",
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
@@ -16291,7 +16289,6 @@ const docTemplate = `{
"Sends notifications via SMTP and webhooks following certain events.",
"Enables the new workspace usage tracking.",
"Enables OAuth2 provider functionality.",
"Enables agent-powered chat functionality.",
"Enables the MCP HTTP server functionality.",
"Enables publishing workspace build updates to the all builds pubsub channel."
],
@@ -16301,7 +16298,6 @@ const docTemplate = `{
"ExperimentNotifications",
"ExperimentWorkspaceUsage",
"ExperimentOAuth2",
"ExperimentAgents",
"ExperimentMCPServerHTTP",
"ExperimentWorkspaceBuildUpdates"
]
-4
View File
@@ -14731,12 +14731,10 @@
"notifications",
"workspace-usage",
"oauth2",
"agents",
"mcp-server-http",
"workspace-build-updates"
],
"x-enum-comments": {
"ExperimentAgents": "Enables agent-powered chat functionality.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentExample": "This isn't used for anything.",
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
@@ -14751,7 +14749,6 @@
"Sends notifications via SMTP and webhooks following certain events.",
"Enables the new workspace usage tracking.",
"Enables OAuth2 provider functionality.",
"Enables agent-powered chat functionality.",
"Enables the MCP HTTP server functionality.",
"Enables publishing workspace build updates to the all builds pubsub channel."
],
@@ -14761,7 +14758,6 @@
"ExperimentNotifications",
"ExperimentWorkspaceUsage",
"ExperimentOAuth2",
"ExperimentAgents",
"ExperimentMCPServerHTTP",
"ExperimentWorkspaceBuildUpdates"
]
+7 -14
View File
@@ -768,7 +768,7 @@ func New(options *Options) *API {
}
api.agentProvider = stn
{ // Experimental: agents — chat daemon and git sync worker initialization.
{ // Chat daemon and git sync worker initialization.
maxChatsPerAcquire := options.DeploymentValues.AI.Chat.AcquireBatchSize.Value()
if maxChatsPerAcquire > math.MaxInt32 {
maxChatsPerAcquire = math.MaxInt32
@@ -1153,11 +1153,9 @@ func New(options *Options) *API {
})
})
})
// Experimental(agents): chat API routes gated by ExperimentAgents.
r.Route("/chats", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents),
)
r.Get("/by-workspace", api.chatsByWorkspace)
r.Get("/", api.listChats)
@@ -1280,7 +1278,6 @@ func New(options *Options) *API {
)
// MCP server configuration endpoints.
r.Route("/servers", func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents))
r.Get("/", api.listMCPServerConfigs)
r.Post("/", api.createMCPServerConfig)
r.Route("/{mcpServer}", func(r chi.Router) {
@@ -2006,14 +2003,10 @@ func New(options *Options) *API {
"parsing additional CSP headers", slog.Error(cspParseErrors))
}
// Add blob: to img-src for chat file attachment previews when
// the agents experiment is enabled.
if api.Experiments.Enabled(codersdk.ExperimentAgents) {
additionalCSPHeaders[httpmw.CSPDirectiveImgSrc] = append(
additionalCSPHeaders[httpmw.CSPDirectiveImgSrc], "blob:",
)
}
// Add blob: to img-src for chat file attachment previews.
additionalCSPHeaders[httpmw.CSPDirectiveImgSrc] = append(
additionalCSPHeaders[httpmw.CSPDirectiveImgSrc], "blob:",
)
// Add CSP headers to all static assets and pages. CSP headers only affect
// browsers, so these don't make sense on api routes.
cspProxyHosts := func() []*proxyhealth.ProxyHost {
@@ -2161,9 +2154,9 @@ type API struct {
// dbRolluper rolls up template usage stats from raw agent and app
// stats. This is used to provide insights in the WebUI.
dbRolluper *dbrollup.Rolluper
// Experimental(agents): chatDaemon handles background processing of pending chats.
// chatDaemon handles background processing of pending chats.
chatDaemon *chatd.Server
// Experimental(agents): gitSyncWorker refreshes stale chat diff statuses in the background.
// gitSyncWorker refreshes stale chat diff statuses in the background.
gitSyncWorker *gitsync.Worker
// AISeatTracker records AI seat usage.
AISeatTracker aiseats.SeatTracker
-1
View File
@@ -56,7 +56,6 @@ func chatDeploymentValues(t testing.TB) *codersdk.DeploymentValues {
t.Helper()
values := coderdtest.DeploymentValues(t)
values.Experiments = []string{string(codersdk.ExperimentAgents)}
return values
}
+4 -8
View File
@@ -18,19 +18,15 @@ import (
"github.com/coder/coder/v2/testutil"
)
// mcpDeploymentValues returns deployment values with the agents
// experiment enabled, which is required by the MCP server config
// endpoints.
// mcpDeploymentValues returns deployment values for tests of the MCP
// server config endpoints.
func mcpDeploymentValues(t testing.TB) *codersdk.DeploymentValues {
t.Helper()
values := coderdtest.DeploymentValues(t)
values.Experiments = []string{string(codersdk.ExperimentAgents)}
return values
return coderdtest.DeploymentValues(t)
}
// newMCPClient creates a test server with the agents experiment
// enabled and returns the admin client.
// newMCPClient creates a test server and returns the admin client.
func newMCPClient(t testing.TB) *codersdk.Client {
t.Helper()
-6
View File
@@ -201,7 +201,6 @@ func TestSubagentChatExcludesWorkspaceProvisioningTools(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
@@ -366,7 +365,6 @@ func TestPlanModeSubagentChatExcludesAskUserQuestion(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
@@ -549,7 +547,6 @@ func TestExploreSubagentIsReadOnly(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
@@ -4936,7 +4933,6 @@ func TestCreateWorkspaceTool_EndToEnd(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
@@ -5117,7 +5113,6 @@ func TestStartWorkspaceTool_EndToEnd(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitSuperLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
@@ -8504,7 +8499,6 @@ func TestAgentContextFilesAndSkillsLoadedIntoChat(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitSuperLong)
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
+3 -6
View File
@@ -35,9 +35,8 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitSuperLong)
// Stand up a full coderd with the agents experiment.
// Stand up a full coderd.
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
})
@@ -296,9 +295,8 @@ func TestOpenAIReasoningRoundTrip(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitSuperLong)
// Stand up a full coderd with the agents experiment.
// Stand up a full coderd.
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
})
@@ -451,9 +449,8 @@ func TestOpenAIReasoningRoundTripStoreFalse(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitSuperLong)
// Stand up a full coderd with the agents experiment.
// Stand up a full coderd.
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: deploymentValues,
})