fix(coderd): enforce Force On MCP server policy on the backend - #27990
Merged
Conversation
The Force On MCP server availability policy was only enforced
client-side: the frontend appended force_on server IDs to the
mcp_server_ids request parameter, so a user could strip them from the
request when creating a chat or sending a message and the forced
servers were silently omitted (Cure53 CDM-02-010).
Enforce the policy server-side at three points:
- chatd.CreateChat unions the requested IDs with every enabled
force_on config before persisting the chat.
- chatd.SendMessage applies the same union inside the update
transaction whenever a caller-provided ID list would overwrite the
chat's stored selection.
- prepareGeneration merges force_on configs into the effective config
set for each turn, covering chats persisted before enforcement
existed and servers marked force_on after chat creation. Existing
plan-mode filtering still narrows the merged set, and Explore
chats keep their immutable spawn-time snapshot.
The forced-config lookup fails closed. User MCP tokens are now
loaded whenever external MCP servers are connected, not only when the
chat's stored ID list is non-empty.
Regression coverage: chatd-level tests for stripped create lists,
emptied update lists, and generation-time enforcement for pre-existing
chats, plus an endpoint-level test walking the original reproduction
steps through POST /api/experimental/chats and
POST /api/experimental/chats/{chat}/messages.
---
_Generated with [`mux`](https://github.com/coder/mux)_
ThomasK33
marked this pull request as ready for review
August 11, 2026 09:51
ibetitsmike
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enforce the Force On MCP server availability policy on the backend for chat creation, message sends, and turn generation, remediating Cure53 finding CDM-02-010 (Linear: CODAGT-787, disclosure: coder/security-disclosures#170).
Problem
MCP server configs with
availability = "force_on"are meant to be injected into every conversation. Enforcement lived only in the frontend, which appended the forced IDs to themcp_server_idsrequest parameter. A user could interceptPOST /api/experimental/chatsorPOST /api/experimental/chats/{chat}/messagesand strip the IDs (for example"mcp_server_ids": []), and the Force On servers were silently omitted from the chat.Fix
Enforcement now happens server-side at three points:
chatd.CreateChatunions the requested IDs with every enabledforce_onconfig before persisting the chat.chatd.SendMessageapplies the same union inside the update transaction whenever a caller-provided ID list would overwrite the chat's stored selection (nilstill means "no change").prepareGenerationmergesforce_onconfigs into the effective config set for each turn, which covers chats persisted before enforcement existed and servers markedforce_onafter chat creation.Existing narrowing still applies on top of the merged set: plan-mode turns keep filtering on
allow_in_plan_mode, and Explore chats keep their immutable spawn-time MCP snapshot (forced servers reach Explore children through the parent chat's already-enforced ID list). The forced-config lookup fails closed rather than running a turn without the forced set. User MCP tokens are now loaded whenever external MCP servers are connected, instead of only when the chat's stored ID list was non-empty.Testing
All new tests fail before the fix and pass after it:
TestCreateChat_ForceOnMCPServerEnforced(chatd): a stripped create list still persists the forced server and its tools are offered to the LLM.TestSendMessage_ForceOnMCPServerEnforced(chatd): an emptied or tamperedmcp_server_idsupdate cannot remove the forced server.TestGeneration_ForceOnMCPServerEnforcedForExistingChats(chatd): chats stored with an empty ID list before a server was markedforce_onstill get the forced tools at generation time.TestChats_ForceOnMCPServerEnforced(coderd): endpoint-level walk of the original reproduction steps (admin creates aforce_onserver, regular member sends tampered requests through both endpoints).Full
./coderd/x/chatd/...suite, chat handler tests,golangci-lint, andintxcheckpass.Generated with
mux