Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6deb103
fix(coderd/x): surface Gemini malformed-function-call stream deaths a…
ibetitsmike Aug 24, 2026
ccabdf1
fix: apply MCP server selection when editing a chat message (#28471)
ibetitsmike Aug 24, 2026
2dbe792
refactor: consolidate viewport hooks and remove defineProperty matchM…
ibetitsmike Aug 24, 2026
cb0c11c
feat(coderd/x/chatd/chattool): improve find_tools relevance and model…
ibetitsmike Aug 25, 2026
7a00423
fix(coderd/x/chatd/mcpclient): enforce MCP connect budget and unblock…
ibetitsmike Aug 25, 2026
87618a2
fix(coderd/x/chatd): truncate overlong generated chat titles instead …
ibetitsmike Aug 25, 2026
63c3565
feat: mount chat API routes under /api/v2 (#28496)
ibetitsmike Aug 26, 2026
33d092e
feat: promote codersdk chat API methods to Client (#28497)
ibetitsmike Aug 26, 2026
29dd39a
feat(site): use /api/v2 chat API paths (#28498)
ibetitsmike Aug 26, 2026
afacdea
fix(site/src): repair failing Storybook stories (#28462)
ethanndickson Aug 24, 2026
78f8e61
feat: enable Coder Agents for organization members (#28186)
ibetitsmike Aug 26, 2026
30368be
feat: allow sharing MCP servers with users and groups (#28593)
ibetitsmike Aug 26, 2026
83517e3
test(site/src/api): expect v2 MCP ACL path (#28657)
jakehwll Aug 26, 2026
d912e58
Merge remote-tracking branch 'origin/release/2.37' into backport/2846…
ibetitsmike Aug 27, 2026
eee1dd6
Merge remote-tracking branch 'origin/backport/28462-to-2.37' into bac…
ibetitsmike Aug 27, 2026
8353315
Merge branch 'backport/28186-to-2.37' into backport/28593-to-2.37
ibetitsmike Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion coderd/chat_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func (api *API) registerUserAIProviderKeyRoutes(r chi.Router) {
// registerOrganizationChatRoutes mounts the organization-scoped chat and
// MCP server configuration routes; r must already extract the
// organization parameter.
func (api *API) registerOrganizationChatRoutes(r chi.Router) {
func (api *API) registerOrganizationChatRoutes(r chi.Router, prefix chatAPIPrefix) {
r.Route("/mcp-servers", func(r chi.Router) {
r.Get("/", api.listMCPServerConfigs)
r.Post("/", api.createMCPServerConfig)
Expand All @@ -242,6 +242,10 @@ func (api *API) registerOrganizationChatRoutes(r chi.Router) {
policy.ActionShare)).Get("/acl", api.mcpServerConfigACL)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Patch("/acl", api.patchMCPServerConfigACL)
if prefix == chatAPIPrefixV2 {
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Get("/acl/available", api.mcpServerConfigACLAvailable)
}
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionRead)).Get("/oauth2/connect", api.mcpServerOAuth2Connect)
})
Expand Down
1 change: 1 addition & 0 deletions coderd/chat_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func TestChatRoutesCompatibility(t *testing.T) {
{http.MethodGet, "/api/v2/chats/config/advisor"},
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/debug/runs", chat.ID)},
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/stream/desktop", chat.ID)},
{http.MethodGet, fmt.Sprintf("/api/experimental/organizations/%s/mcp-servers/not-a-uuid/acl/available", firstUser.OrganizationID)},
{http.MethodGet, "/api/v2/mcp/servers/not-a-uuid/oauth2/callback"},
{http.MethodPost, "/api/v2/mcp/http/server"},
} {
Expand Down
4 changes: 2 additions & 2 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,7 @@ func New(options *Options) *API {
r.Use(apiKeyMiddleware)
r.Route("/{organization}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationParam(options.Database))
api.registerOrganizationChatRoutes(r)
api.registerOrganizationChatRoutes(r, chatAPIPrefixExperimental)
r.Route("/members/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMemberParam(options.Database))
api.registerOrganizationMemberChatRoutes(r)
Expand Down Expand Up @@ -1514,7 +1514,7 @@ func New(options *Options) *API {
r.Use(
httpmw.ExtractOrganizationParam(options.Database),
)
api.registerOrganizationChatRoutes(r)
api.registerOrganizationChatRoutes(r, chatAPIPrefixV2)
r.Get("/", api.organization)
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
Expand Down
19 changes: 14 additions & 5 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,12 @@ func SlimRolesFromNames(names []string) []codersdk.SlimRole {
convertedRoles := make([]codersdk.SlimRole, 0, len(names))

for _, name := range names {
// Stored role arrays may retain retired built-in role names until
// a cleanup migration lands. Hide them so consumers do not display
// or resubmit them.
if rbac.IsRetiredRoleName(name) {
continue
}
convertedRoles = append(convertedRoles, SlimRoleFromName(name))
}

Expand Down Expand Up @@ -919,11 +925,14 @@ func Organization(organization database.Organization) codersdk.Organization {
DisplayName: organization.DisplayName,
Icon: organization.Icon,
},
Description: organization.Description,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
IsDefault: organization.IsDefault,
DefaultOrgMemberRoles: organization.DefaultOrgMemberRoles,
Description: organization.Description,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
IsDefault: organization.IsDefault,
// Stored default role lists may retain retired built-in role names
// until a cleanup migration lands. Hide them so settings forms do
// not display or resubmit them.
DefaultOrgMemberRoles: slices.DeleteFunc(slices.Clone(organization.DefaultOrgMemberRoles), rbac.IsRetiredRoleName),
}
}

Expand Down
37 changes: 37 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,15 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID uuid.UUID, added, re
grantedRoles := make([]rbac.RoleIdentifier, 0, len(added)+len(removed))
grantedRoles = append(grantedRoles, added...)
grantedRoles = append(grantedRoles, removed...)
// Retired role names may linger in stored role arrays and org default
// role lists until a data cleanup migration lands. They expand to no
// permissions and cannot be re-created as custom roles, so validating
// them is unnecessary: the added set only contains them via stored
// data (implied org defaults), never via explicit grants, which the
// role-update paths reject before reaching this filter.
grantedRoles = slices.DeleteFunc(grantedRoles, func(r rbac.RoleIdentifier) bool {
return rbac.IsRetiredRoleName(r.Name)
})
customRoles := make([]rbac.RoleIdentifier, 0)
// Validate that the roles being assigned are valid.
for _, r := range grantedRoles {
Expand Down Expand Up @@ -7860,6 +7869,16 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb
if err != nil {
return database.OrganizationMember{}, err
}
// Explicitly granting a retired role is rejected. Retired-name tolerance
// only covers stored grants and implied org defaults that linger until
// the cleanup migration lands; without this check the request would
// skip validation and persist a hidden grant that a binary rollback
// resolves again.
for _, role := range scopedGranted {
if rbac.IsRetiredRoleName(role.Name) {
return database.OrganizationMember{}, xerrors.Errorf("role %q is retired and cannot be assigned", role.Name)
}
}

// The org's default_org_member_roles are implied at request time by
// GetAuthorizationUserRoles. Include them in the implied set so
Expand Down Expand Up @@ -7936,6 +7955,15 @@ func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrg
scopedOrgRoleIdentifiers(existing.DefaultOrgMemberRoles, arg.ID),
scopedOrgRoleIdentifiers(arg.DefaultOrgMemberRoles, arg.ID),
)
// Newly added defaults must not include retired names, which
// canAssignRoles tolerates only so stale stored defaults keep
// working until the cleanup migration lands. Removals stay
// tolerated so those stale defaults can be cleaned up.
for _, role := range added {
if rbac.IsRetiredRoleName(role.Name) {
return database.Organization{}, xerrors.Errorf("role %q is retired and cannot be a default role", role.Name)
}
}
if err := q.canAssignRoles(ctx, arg.ID, added, removed); err != nil {
return database.Organization{}, err
}
Expand Down Expand Up @@ -8486,6 +8514,15 @@ func (q *querier) UpdateUserRoles(ctx context.Context, arg database.UpdateUserRo
return database.User{}, err
}

// Explicitly granting a retired role is rejected. Retired-name tolerance
// only covers stored grants that linger until the cleanup migration
// lands.
for _, roleName := range arg.GrantedRoles {
if rbac.IsRetiredRoleName(roleName) {
return database.User{}, xerrors.Errorf("role %q is retired and cannot be assigned", roleName)
}
}

// The member role is always implied.
impliedTypes := append(q.convertToDeploymentRoles(arg.GrantedRoles), rbac.RoleMember())
// If the changeset is nothing, less rbac checks need to be done.
Expand Down
12 changes: 3 additions & 9 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1465,8 +1465,8 @@ func TestGetAuthorizedChats(t *testing.T) {

org := dbgen.Organization(t, db, database.Organization{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: owner.ID, OrganizationID: org.ID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: member.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: secondMember.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: member.ID, OrganizationID: org.ID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: secondMember.ID, OrganizationID: org.ID})

// Create FK dependencies: a chat provider and model config.
_ = dbgen.ChatProvider(t, db, database.ChatProvider{
Expand Down Expand Up @@ -1653,7 +1653,7 @@ func TestGetAuthorizedChats(t *testing.T) {
// Use a dedicated user for pagination to avoid interference
// with the other parallel subtests.
paginationUser := dbgen.User(t, db, database.User{})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: paginationUser.ID, OrganizationID: org.ID, Roles: []string{rbac.RoleAgentsAccess()}})
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: paginationUser.ID, OrganizationID: org.ID})
for i := range 7 {
dbgen.Chat(t, db, database.Chat{
OrganizationID: org.ID,
Expand Down Expand Up @@ -1727,12 +1727,10 @@ func TestGetAuthorizedChatsACLSharing(t *testing.T) {
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: owner.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: recipient.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})

dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"})
Expand Down Expand Up @@ -1845,12 +1843,10 @@ func TestGetAuthorizedChatsACLSharingGroupACL(t *testing.T) {
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: owner.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: recipient.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: recipient.ID, GroupID: group.ID})
Expand Down Expand Up @@ -1949,12 +1945,10 @@ func TestGetAuthorizedChatsByChatFileIDACLSharing(t *testing.T) {
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: owner.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})
dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: recipient.ID,
OrganizationID: org.ID,
Roles: []string{rbac.RoleAgentsAccess()},
})

dbgen.ChatProvider(t, db, database.ChatProvider{Provider: "openai", DisplayName: "OpenAI"})
Expand Down
4 changes: 2 additions & 2 deletions coderd/exp_chats_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ func TestListChatsSharedScope(t *testing.T) {
client, db := newChatClientWithDatabase(t)
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModel(t, client)
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID))
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
viewerClientExp := codersdk.NewExperimentalClient(viewerClient)
sharedChat := dbgen.Chat(t, db, database.Chat{
OrganizationID: firstUser.OrganizationID,
Expand Down Expand Up @@ -562,7 +562,7 @@ func TestChatSharingDisabled(t *testing.T) {
})
firstUser := coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModel(t, client)
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID))
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
viewerClientExp := codersdk.NewExperimentalClient(viewerClient)

chat := dbgen.Chat(t, store, database.Chat{
Expand Down
3 changes: 1 addition & 2 deletions coderd/exp_chats_model_config_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,12 @@ func TestChatModelConfigListReadContracts(t *testing.T) {
seesDenied bool
}{
{
name: "AgentsAccess",
name: "Member",
client: func(t *testing.T, _ context.Context) *codersdk.ExperimentalClient {
rawClient, _ := coderdtest.CreateAnotherUser(
t,
client.Client,
defaultOrg.ID,
rbac.ScopedRoleAgentsAccess(defaultOrg.ID),
)
return codersdk.NewExperimentalClient(rawClient)
},
Expand Down
Loading
Loading