diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 635acd6dd3daa..14df0435ee17a 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -55,6 +55,13 @@ OPTIONS: the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured. + --disable-workspace-agent-context-sync bool, $CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC + Stop persisting workspace agent context snapshots (instructions, + skills, and MCP state used for pinned chat context). When set, coderd + rejects agent context pushes as unimplemented and agents stop sending + them; chats cannot pin workspace context. Use this to shed the + database write load of context sync on large deployments. + --disable-workspace-sharing bool, $CODER_DISABLE_WORKSPACE_SHARING Disable workspace sharing. Workspace ACL checking is disabled and only owners can have ssh, apps and terminal access to workspaces. Access diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 2069cb05bdaeb..2324e85ee4efc 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -566,6 +566,13 @@ disableWorkspaceSharing: false # their chats. # (default: , type: bool) disableChatSharing: false +# Stop persisting workspace agent context snapshots (instructions, skills, and MCP +# state used for pinned chat context). When set, coderd rejects agent context +# pushes as unimplemented and agents stop sending them; chats cannot pin workspace +# context. Use this to shed the database write load of context sync on large +# deployments. +# (default: , type: bool) +disableWorkspaceAgentContextSync: false # These options change the behavior of how clients interact with the Coder. # Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. client: diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 2175505921fdb..f13096ed1cc35 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -83,7 +83,10 @@ type Options struct { Pubsub pubsub.Pubsub // ContextDirtyMarker is the chatd-backed hydrate/dirty fan-out invoked // from PushContextState. Nil when chatd is disabled. - ContextDirtyMarker ContextDirtyMarker + ContextDirtyMarker ContextDirtyMarker + // ContextSyncDisabled makes PushContextState reject pushes with a dRPC + // Unimplemented code so agents stop sending context snapshots. + ContextSyncDisabled bool ConnectionLogger *atomic.Pointer[connectionlog.ConnectionLogger] DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] @@ -257,6 +260,7 @@ func New(opts Options, workspace database.Workspace, agent database.WorkspaceAge Clock: opts.Clock, Database: opts.Database, DirtyMarker: opts.ContextDirtyMarker, + Disabled: opts.ContextSyncDisabled, } // Start background cache refresh loop to handle workspace changes diff --git a/coderd/agentapi/context.go b/coderd/agentapi/context.go index f6dd69e4e3824..09766bca87c8a 100644 --- a/coderd/agentapi/context.go +++ b/coderd/agentapi/context.go @@ -12,6 +12,7 @@ import ( "golang.org/x/xerrors" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "storj.io/drpc/drpcerr" "cdr.dev/slog/v3" agentproto "github.com/coder/coder/v2/agent/proto" @@ -67,6 +68,13 @@ type ContextAPI struct { // snapshot persisted by a push. It is nil when chatd is not running, // in which case PushContextState stays a pure write path. DirtyMarker ContextDirtyMarker + // Disabled rejects every push with a dRPC Unimplemented code. The + // agent's DRPCPusher translates that code into ErrPushUnimplemented, + // which terminates its RunPush loop for the life of the connection, + // exactly as if coderd predated the v2.10 Agent API. This is the + // deployment-wide kill switch for context sync write load + // (CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC). + Disabled bool } // ContextDirtyMarker hydrates chats from, and marks chats dirty against, a @@ -103,6 +111,14 @@ type ContextDirtyMarker interface { // authorizes the actor (the agent's token subject) against the // workspace that owns the agent. func (a *ContextAPI) PushContextState(ctx context.Context, req *agentproto.PushContextStateRequest) (*agentproto.PushContextStateResponse, error) { + if a.Disabled { + // The Unimplemented code (not a plain error) is what tells the + // agent to stop pushing instead of retrying with backoff. + return nil, drpcerr.WithCode( + xerrors.New("agentapi: workspace agent context sync is disabled on this deployment"), + drpcerr.Unimplemented, + ) + } if req == nil { return nil, xerrors.New("agentapi: PushContextState request is nil") } diff --git a/coderd/agentapi/context_test.go b/coderd/agentapi/context_test.go index 5c724b93560da..f190d0dfb0280 100644 --- a/coderd/agentapi/context_test.go +++ b/coderd/agentapi/context_test.go @@ -13,6 +13,7 @@ import ( "github.com/lib/pq" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "storj.io/drpc/drpcerr" "cdr.dev/slog/v3" "cdr.dev/slog/v3/sloggers/slogtest" @@ -58,6 +59,27 @@ func TestPushContextState(t *testing.T) { ) } + t.Run("DisabledReturnsUnimplemented", func(t *testing.T) { + t.Parallel() + + // No InTx or query expectations: a disabled push must return + // before touching the store. The Unimplemented dRPC code is + // load-bearing; the agent's DRPCPusher translates it into + // ErrPushUnimplemented, which stops its RunPush loop instead + // of retrying with backoff. + api, _ := makeAPI(t) + api.Disabled = true + + resp, err := api.PushContextState(context.Background(), &agentproto.PushContextStateRequest{ + Version: 1, + AggregateHash: []byte{0x01, 0x02}, + Initial: true, + }) + require.Error(t, err) + require.Nil(t, resp) + require.EqualValues(t, drpcerr.Unimplemented, drpcerr.Code(err)) + }) + t.Run("AcceptsInitialPush", func(t *testing.T) { t.Parallel() diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 434b2367d95d0..65a2edb6978e4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -21719,6 +21719,9 @@ const docTemplate = `{ "disable_path_apps": { "type": "boolean" }, + "disable_workspace_agent_context_sync": { + "type": "boolean" + }, "disable_workspace_sharing": { "type": "boolean" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4a657b17c1483..89ad2a191a413 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -19758,6 +19758,9 @@ "disable_path_apps": { "type": "boolean" }, + "disable_workspace_agent_context_sync": { + "type": "boolean" + }, "disable_workspace_sharing": { "type": "boolean" }, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 04f6ee1ef8ff3..c069d488b09bd 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -38,6 +38,7 @@ import ( "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentcontainers/acmock" "github.com/coder/coder/v2/agent/agentcontainers/watcher" + "github.com/coder/coder/v2/agent/agentcontext" "github.com/coder/coder/v2/agent/agenttest" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi/metadatabatcher" @@ -3374,6 +3375,54 @@ func TestWorkspaceAgentPushContextState(t *testing.T) { require.False(t, resp.GetAccepted()) } +// TestWorkspaceAgentPushContextStateDisabled verifies the +// --disable-workspace-agent-context-sync kill switch end to end over a +// real dRPC connection: the handler's Unimplemented code must survive +// the transport and be translated by the agent's DRPCPusher into +// ErrPushUnimplemented, which is what terminates the agent's RunPush +// loop instead of retrying with backoff. Nothing may be persisted. +func TestWorkspaceAgentPushContextStateDisabled(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.DisableWorkspaceAgentContextSync = true + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + require.Len(t, r.Agents, 1) + agentID := r.Agents[0].ID + + ctx := testutil.Context(t, testutil.WaitLong) + + agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(r.AgentToken)) + aAPI, _, err := agentClient.ConnectRPC210(ctx) + require.NoError(t, err) + defer func() { + cErr := aAPI.DRPCConn().Close() + require.NoError(t, cErr) + }() + + // Push through the same adapter the agent's RunPush loop uses so + // the test breaks if either side of the Unimplemented contract + // changes. + pusher := agentcontext.NewDRPCPusher(aAPI) + resp, err := pusher.PushContextState(ctx, &agentcontext.PushRequest{ + Version: 1, + Initial: true, + }) + require.ErrorIs(t, err, agentcontext.ErrPushUnimplemented) + require.Nil(t, resp) + + // The rejected push must not have persisted anything. + _, err = db.GetLatestWorkspaceAgentContextSnapshot(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // Test assertions read agent-pushed rows directly from the store. + require.ErrorIs(t, err, sql.ErrNoRows) +} + func requireGetManifest(ctx context.Context, t testing.TB, aAPI agentproto.DRPCAgentClient) agentsdk.Manifest { mp, err := aAPI.GetManifest(ctx, &agentproto.GetManifestRequest{}) require.NoError(t, err) diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index 91285c0346b4a..1be4cc4248a71 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -192,6 +192,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { // Optional: UpdateAgentMetricsFn: api.UpdateAgentMetrics, ContextDirtyMarker: contextDirtyMarker, + ContextSyncDisabled: api.DeploymentValues.DisableWorkspaceAgentContextSync.Value(), }, workspace, workspaceAgent) streamID := tailnet.StreamID{ diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 5b7ea5a061071..cc0c77fba6679 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -736,6 +736,7 @@ type DeploymentValues struct { DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` DisableWorkspaceSharing serpent.Bool `json:"disable_workspace_sharing,omitempty" typescript:",notnull"` DisableChatSharing serpent.Bool `json:"disable_chat_sharing,omitempty" typescript:",notnull"` + DisableWorkspaceAgentContextSync serpent.Bool `json:"disable_workspace_agent_context_sync,omitempty" typescript:",notnull"` ProxyHealthStatusInterval serpent.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"` EnableTerraformDebugMode serpent.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"` UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"` @@ -3781,6 +3782,15 @@ communicating directly.`, Value: &c.DisableChatSharing, YAML: "disableChatSharing", }, + { + Name: "Disable Workspace Agent Context Sync", + Description: "Stop persisting workspace agent context snapshots (instructions, skills, and MCP state used for pinned chat context). When set, coderd rejects agent context pushes as unimplemented and agents stop sending them; chats cannot pin workspace context. Use this to shed the database write load of context sync on large deployments.", + Flag: "disable-workspace-agent-context-sync", + Env: "CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC", + + Value: &c.DisableWorkspaceAgentContextSync, + YAML: "disableWorkspaceAgentContextSync", + }, { Name: "Session Duration", Description: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.", diff --git a/docs/admin/setup/configuration-reference.md b/docs/admin/setup/configuration-reference.md index 7353bc7681ba9..75d8ea58873ed 100644 --- a/docs/admin/setup/configuration-reference.md +++ b/docs/admin/setup/configuration-reference.md @@ -82,6 +82,14 @@ Disable workspace apps that are not served from subdomains. Path-based apps can - CLI flag: [`--disable-path-apps`](../../reference/cli/server.md#--disable-path-apps) - YAML key: `disablePathApps` +### Disable workspace agent context sync + +Stop persisting workspace agent context snapshots (instructions, skills, and MCP state used for pinned chat context). When set, coderd rejects agent context pushes as unimplemented and agents stop sending them; chats cannot pin workspace context. Use this to shed the database write load of context sync on large deployments. + +- Environment variable: `CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC` +- CLI flag: [`--disable-workspace-agent-context-sync`](../../reference/cli/server.md#--disable-workspace-agent-context-sync) +- YAML key: `disableWorkspaceAgentContextSync` + ### Disable workspace sharing Disable workspace sharing. Workspace ACL checking is disabled and only owners can have ssh, apps and terminal access to workspaces. Access based on the 'owner' role is also allowed unless disabled via --disable-owner-workspace-access. diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index e11f1ef063afb..fb91e0385a779 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -311,6 +311,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_workspace_agent_context_sync": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 38a0ebde20e03..2320d4ff0d4ea 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7418,6 +7418,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_workspace_agent_context_sync": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -8046,6 +8047,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_workspace_agent_context_sync": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -8441,6 +8443,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `disable_owner_workspace_exec` | boolean | false | | | | `disable_password_auth` | boolean | false | | | | `disable_path_apps` | boolean | false | | | +| `disable_workspace_agent_context_sync` | boolean | false | | | | `disable_workspace_sharing` | boolean | false | | | | `docs_url` | [serpent.URL](#serpenturl) | false | | | | `enable_ai_tasks` | boolean | false | | | diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 6035137abf8f2..aed9ff99de390 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1216,6 +1216,16 @@ Disable workspace sharing. Workspace ACL checking is disabled and only owners ca Disable chat sharing. Chat ACL checking is disabled and only owners can access their chats. +### --disable-workspace-agent-context-sync + +| | | +|-------------|----------------------------------------------------------| +| Type | bool | +| Environment | $CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC | +| YAML | disableWorkspaceAgentContextSync | + +Stop persisting workspace agent context snapshots (instructions, skills, and MCP state used for pinned chat context). When set, coderd rejects agent context pushes as unimplemented and agents stop sending them; chats cannot pin workspace context. Use this to shed the database write load of context sync on large deployments. + ### --session-duration | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index e462dd680b15a..4cf3311646971 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -56,6 +56,13 @@ OPTIONS: the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured. + --disable-workspace-agent-context-sync bool, $CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC + Stop persisting workspace agent context snapshots (instructions, + skills, and MCP state used for pinned chat context). When set, coderd + rejects agent context pushes as unimplemented and agents stop sending + them; chats cannot pin workspace context. Use this to shed the + database write load of context sync on large deployments. + --disable-workspace-sharing bool, $CODER_DISABLE_WORKSPACE_SHARING Disable workspace sharing. Workspace ACL checking is disabled and only owners can have ssh, apps and terminal access to workspaces. Access diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7ebd2042386b0..d8320f2cb6837 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4830,6 +4830,7 @@ export interface DeploymentValues { readonly disable_owner_workspace_exec?: boolean; readonly disable_workspace_sharing?: boolean; readonly disable_chat_sharing?: boolean; + readonly disable_workspace_agent_context_sync?: boolean; readonly proxy_health_status_interval?: number; readonly enable_terraform_debug_mode?: boolean; readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig;