diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 635acd6dd3d..14df0435ee1 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 2069cb05bda..2324e85ee4e 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 2175505921f..f13096ed1cc 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 f6dd69e4e38..09766bca87c 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 5c724b93560..f190d0dfb02 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 434b2367d95..65a2edb6978 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 4a657b17c14..89ad2a191a4 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 04f6ee1ef8f..c069d488b09 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 91285c0346b..1be4cc4248a 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 5b7ea5a0610..cc0c77fba66 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 7353bc7681b..75d8ea58873 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 e11f1ef063a..fb91e0385a7 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 38a0ebde20e..2320d4ff0d4 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 6035137abf8..aed9ff99de3 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 e462dd680b1..4cf33116469 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 7ebd2042386..d8320f2cb68 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;