From 1e68cef90746cde7579a5d61feff1e498d4d09fb Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 10 Aug 2026 16:04:05 -0400 Subject: [PATCH 1/3] chore(.github/workflows): remove Pixel CI check (#28001) Removes the Pixel CI check from this release branch in an attempt to make CI green. Discussed with @aslilac and it is not worth the effort to maintain and fix the Pixel CI checks on release branches, especially as we continue development on the Pixel engine and service on main. --- .github/workflows/ci.yaml | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c189817c0c2..394a890f26f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1060,37 +1060,6 @@ jobs: path: ./site/test-results/**/debug-pprof-*.txt retention-days: 7 - storybook: - name: Storybook - - runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-24.04-16' || 'ubuntu-latest' }} - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout - with: - persist-credentials: false - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install dependencies - with: - run_install: true - cache: true - - - run: pnpm storybook:build - working-directory: site/ - name: Build Storybook - - - run: pnpm playwright:install - working-directory: site/ - name: Install Chromium - - - run: pnpm pixel-storybook - working-directory: site/ - name: Snapshot - env: - PIXEL_KEY: ${{ secrets.PIXEL_KEY }} - offlinedocs: name: offlinedocs needs: changes From de7a285d4885fe2e16a800bf66c785d460bb29bc Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 25 Aug 2026 01:33:51 -0500 Subject: [PATCH 2/3] fix: add flag to disable workspace agent context sync (cherry-pick #28522) (#28525) Cherry-pick of #28522 (`26de6140fb`) onto `release/2.35`. Adds `CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC` / `--disable-workspace-agent-context-sync`. When set, `PushContextState` rejects agent context pushes with a dRPC `Unimplemented` code before any validation or database work; deployed agents translate that into `ErrPushUnimplemented` and stop their push loop for the life of the connection. This gives large deployments a server-only kill switch for context sync database write load, with no agent updates or workspace restarts required. Two conflicts resolved relative to the original commit: - `coderd/workspaceagentsrpc.go`: `release/2.35` assigns `ContextDirtyMarker: api.chatDaemon` directly (the nil-guarded local was introduced later on `main`); kept the branch's form and added `ContextSyncDisabled` alongside it. - `docs/admin/setup/configuration-reference.md`: dropped; this generated doc does not exist on `release/2.35`. Validated on this branch: `go build`, `TestPushContextState` (unit), `TestWorkspaceAgentPushContextState*` (end-to-end over real dRPC), CLI and enterprise golden-file tests, full pre-commit hooks. --- cli/testdata/coder_server_--help.golden | 7 +++ cli/testdata/server-config.yaml.golden | 7 +++ coderd/agentapi/api.go | 6 ++- coderd/agentapi/context.go | 16 ++++++ coderd/agentapi/context_test.go | 22 +++++++++ coderd/apidoc/docs.go | 3 ++ coderd/apidoc/swagger.json | 3 ++ coderd/workspaceagents_test.go | 49 +++++++++++++++++++ coderd/workspaceagentsrpc.go | 3 +- codersdk/deployment.go | 10 ++++ docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 3 ++ docs/reference/cli/server.md | 10 ++++ .../cli/testdata/coder_server_--help.golden | 7 +++ site/src/api/typesGenerated.ts | 1 + 15 files changed, 146 insertions(+), 2 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index fccd065f509..27df958871f 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 f16fe383c15..a6c78a60b96 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -559,6 +559,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 ce697bc4826..c6a77362dec 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 a280e5e7c42..56bad840a54 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -19261,6 +19261,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 5eaa93a9803..f1498ac516e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -17461,6 +17461,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 1e52d1e35cf..400b620d9af 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -35,6 +35,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" @@ -3240,6 +3241,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 d01dec9cb8f..6aef92a58de 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -182,7 +182,8 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { UpdateAgentMetricsFn: api.UpdateAgentMetrics, // chatDaemon is always constructed (only its worker is gated), so // this is non-nil; agentapi treats a nil marker as "chatd absent". - ContextDirtyMarker: api.chatDaemon, + ContextDirtyMarker: api.chatDaemon, + ContextSyncDisabled: api.DeploymentValues.DisableWorkspaceAgentContextSync.Value(), }, workspace, workspaceAgent) streamID := tailnet.StreamID{ diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9373855be76..9a5272c06b8 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -670,6 +670,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"` @@ -3680,6 +3681,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/reference/api/general.md b/docs/reference/api/general.md index fa3c245d809..90c3d952211 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -282,6 +282,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 879951910b0..0244d8369ea 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -5664,6 +5664,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, @@ -6270,6 +6271,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, @@ -6661,6 +6663,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_authz_recording` | boolean | false | | | diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 5ff296bc02a..38046574c34 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1201,6 +1201,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 f29a8f84a74..e31045c90a9 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 e0de4e1c9f8..c94d511132b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4358,6 +4358,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; From ef299b62fdf86d69da125f1619ce6b0849e41c90 Mon Sep 17 00:00:00 2001 From: Jakub Domeracki Date: Tue, 25 Aug 2026 16:35:15 +0200 Subject: [PATCH 3/3] ci: use dedicated release App token to publish releases (backport 2.35) (#28554) Backport of #28553 to `release/2.35`. ## Change Use a dedicated GitHub App token for the `Publish release` step: - Add a `Generate release App token` step using `secrets.RELEASE_APP_ID` / `secrets.RELEASE_APP_PRIVATE_KEY`. - Switch only that step's `GITHUB_TOKEN` to the minted App token. ## Required before merge (admin) 1. Create the release GitHub App (least privilege) and install it on `coder/coder`. 2. Configure `RELEASE_APP_ID` / `RELEASE_APP_PRIVATE_KEY`. 3. Add the App to the tag-create protection ruleset bypass list. Refs coder/security-automation#297. --- .github/workflows/release.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f7bc611357e..9225194435b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -540,6 +540,18 @@ jobs: gcloud storage cp "./build/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${cli_name}.asc" done + # Mint a short-lived installation token from the dedicated release + # GitHub App. The default GITHUB_TOKEN (github-actions[bot]) cannot be + # added to the "Auto-imported tag create protections" ruleset bypass + # list, so `gh release create` fails to create the tag with a 403. The + # App is added to that ruleset's bypass list as an Integration actor. + - name: Generate release App token + id: release_app_token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + - name: Publish release run: | set -euo pipefail @@ -578,7 +590,9 @@ jobs: --release-notes-file "$CODER_RELEASE_NOTES_FILE" \ "${files[@]}" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Use the dedicated release App token (github-actions[bot] is blocked + # from creating tags by the tag-create protection ruleset). + GITHUB_TOKEN: ${{ steps.release_app_token.outputs.token }} CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} VERSION: ${{ steps.version.outputs.version }} CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}