diff --git a/coderd/coderd.go b/coderd/coderd.go index 35e8026771..75279d1c52 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1398,8 +1398,6 @@ func New(options *Options) *API { r.Put("/debug-retention-days", api.putChatDebugRetentionDays) r.Get("/auto-archive-days", api.getChatAutoArchiveDays) r.Put("/auto-archive-days", api.putChatAutoArchiveDays) - r.Get("/template-allowlist", api.getChatTemplateAllowlist) - r.Put("/template-allowlist", api.putChatTemplateAllowlist) }) // TODO(cian): place under /api/experimental/chats/config r.Route("/providers", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 17422489ed..3d036e150e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3555,17 +3555,6 @@ func (q *querier) GetChatSystemPromptConfig(ctx context.Context) (database.GetCh return q.db.GetChatSystemPromptConfig(ctx) } -// GetChatTemplateAllowlist requires deployment-config read permission, -// unlike the peer getters (GetChatDesktopEnabled, etc.) which only -// check actor presence. The allowlist is admin-configuration that -// should not be readable by non-admin users via the HTTP API. -func (q *querier) GetChatTemplateAllowlist(ctx context.Context) (string, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { - return "", err - } - return q.db.GetChatTemplateAllowlist(ctx) -} - func (q *querier) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { return "", err @@ -8914,13 +8903,6 @@ func (q *querier) UpsertChatSystemPrompt(ctx context.Context, value string) erro return q.db.UpsertChatSystemPrompt(ctx, value) } -func (q *querier) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { - return err - } - return q.db.UpsertChatTemplateAllowlist(ctx, templateAllowlist) -} - func (q *querier) UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ad63fca3b4..c0cd5e280b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1158,10 +1158,6 @@ func (s *MethodTestSuite) TestChats() { dbm.EXPECT().GetChatPlanModeInstructions(gomock.Any()).Return("", nil).AnyTimes() check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("GetChatTemplateAllowlist", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { - dbm.EXPECT().GetChatTemplateAllowlist(gomock.Any()).Return("", nil).AnyTimes() - check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead) - })) s.Run("GetChatWorkspaceTTL", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().GetChatWorkspaceTTL(gomock.Any()).Return("1h", nil).AnyTimes() check.Args().Asserts() @@ -1594,10 +1590,6 @@ func (s *MethodTestSuite) TestChats() { dbm.EXPECT().UpsertChatPlanModeInstructions(gomock.Any(), "").Return(nil).AnyTimes() check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("UpsertChatTemplateAllowlist", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { - dbm.EXPECT().UpsertChatTemplateAllowlist(gomock.Any(), "").Return(nil).AnyTimes() - check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) - })) s.Run("UpsertChatWorkspaceTTL", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().UpsertChatWorkspaceTTL(gomock.Any(), "1h").Return(nil).AnyTimes() check.Args("1h").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 6699d05325..2cbc971028 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1769,14 +1769,6 @@ func (m queryMetricsStore) GetChatSystemPromptConfig(ctx context.Context) (datab return r0, r1 } -func (m queryMetricsStore) GetChatTemplateAllowlist(ctx context.Context) (string, error) { - start := time.Now() - r0, r1 := m.s.GetChatTemplateAllowlist(ctx) - m.queryLatencies.WithLabelValues("GetChatTemplateAllowlist").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatTemplateAllowlist").Inc() - return r0, r1 -} - func (m queryMetricsStore) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) { start := time.Now() r0, r1 := m.s.GetChatTitleGenerationModelOverride(ctx) @@ -6305,14 +6297,6 @@ func (m queryMetricsStore) UpsertChatSystemPrompt(ctx context.Context, value str return r0 } -func (m queryMetricsStore) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error { - start := time.Now() - r0 := m.s.UpsertChatTemplateAllowlist(ctx, templateAllowlist) - m.queryLatencies.WithLabelValues("UpsertChatTemplateAllowlist").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertChatTemplateAllowlist").Inc() - return r0 -} - func (m queryMetricsStore) UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertChatTitleGenerationModelOverride(ctx, value) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 9607e47ec2..38ae689b76 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3270,21 +3270,6 @@ func (mr *MockStoreMockRecorder) GetChatSystemPromptConfig(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatSystemPromptConfig", reflect.TypeOf((*MockStore)(nil).GetChatSystemPromptConfig), ctx) } -// GetChatTemplateAllowlist mocks base method. -func (m *MockStore) GetChatTemplateAllowlist(ctx context.Context) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChatTemplateAllowlist", ctx) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChatTemplateAllowlist indicates an expected call of GetChatTemplateAllowlist. -func (mr *MockStoreMockRecorder) GetChatTemplateAllowlist(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatTemplateAllowlist", reflect.TypeOf((*MockStore)(nil).GetChatTemplateAllowlist), ctx) -} - // GetChatTitleGenerationModelOverride mocks base method. func (m *MockStore) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) { m.ctrl.T.Helper() @@ -11804,20 +11789,6 @@ func (mr *MockStoreMockRecorder) UpsertChatSystemPrompt(ctx, value any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatSystemPrompt", reflect.TypeOf((*MockStore)(nil).UpsertChatSystemPrompt), ctx, value) } -// UpsertChatTemplateAllowlist mocks base method. -func (m *MockStore) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertChatTemplateAllowlist", ctx, templateAllowlist) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertChatTemplateAllowlist indicates an expected call of UpsertChatTemplateAllowlist. -func (mr *MockStoreMockRecorder) UpsertChatTemplateAllowlist(ctx, templateAllowlist any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatTemplateAllowlist", reflect.TypeOf((*MockStore)(nil).UpsertChatTemplateAllowlist), ctx, templateAllowlist) -} - // UpsertChatTitleGenerationModelOverride mocks base method. func (m *MockStore) UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error { m.ctrl.T.Helper() diff --git a/coderd/database/migrations/000564_delete_agents_template_allowlist.down.sql b/coderd/database/migrations/000564_delete_agents_template_allowlist.down.sql new file mode 100644 index 0000000000..7f9f9f9ec7 --- /dev/null +++ b/coderd/database/migrations/000564_delete_agents_template_allowlist.down.sql @@ -0,0 +1,2 @@ +-- No-op. The up migration deleted the only copy of the deployment-wide +-- allowlist, so there is nothing left to restore. diff --git a/coderd/database/migrations/000564_delete_agents_template_allowlist.up.sql b/coderd/database/migrations/000564_delete_agents_template_allowlist.up.sql new file mode 100644 index 0000000000..ccf660d693 --- /dev/null +++ b/coderd/database/migrations/000564_delete_agents_template_allowlist.up.sql @@ -0,0 +1,4 @@ +-- 000563 copied this value into templates.agents_allowed. Deleting the row +-- makes the original list unrecoverable. +DELETE FROM site_configs +WHERE key = 'agents_template_allowlist'; diff --git a/coderd/database/querier.go b/coderd/database/querier.go index e60b661e4c..882e4cb9d5 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -507,9 +507,6 @@ type sqlcQuerier interface { // non-empty custom prompt implied opting out before the explicit toggle // existed. GetChatSystemPromptConfig(ctx context.Context) (GetChatSystemPromptConfigRow, error) - // GetChatTemplateAllowlist returns the JSON-encoded template allowlist. - // Returns an empty string when no allowlist has been configured (all templates allowed). - GetChatTemplateAllowlist(ctx context.Context) (string, error) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) // Returns the concatenated text of each user-visible user prompt in a // chat, newest first. Used by the composer to populate the up/down @@ -1650,7 +1647,6 @@ type sqlcQuerier interface { UpsertChatPlanModeInstructions(ctx context.Context, value string) error UpsertChatRetentionDays(ctx context.Context, retentionDays int32) error UpsertChatSystemPrompt(ctx context.Context, value string) error - UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error UpsertChatWorkspaceTTL(ctx context.Context, workspaceTtl string) error // The default proxy is implied and not actually stored in the database. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8e9f76689b..e499234f55 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -24825,20 +24825,6 @@ func (q *sqlQuerier) GetChatSystemPromptConfig(ctx context.Context) (GetChatSyst return i, err } -const getChatTemplateAllowlist = `-- name: GetChatTemplateAllowlist :one -SELECT - COALESCE((SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'), '') :: text AS template_allowlist -` - -// GetChatTemplateAllowlist returns the JSON-encoded template allowlist. -// Returns an empty string when no allowlist has been configured (all templates allowed). -func (q *sqlQuerier) GetChatTemplateAllowlist(ctx context.Context) (string, error) { - row := q.db.QueryRowContext(ctx, getChatTemplateAllowlist) - var template_allowlist string - err := row.Scan(&template_allowlist) - return template_allowlist, err -} - const getChatTitleGenerationModelOverride = `-- name: GetChatTitleGenerationModelOverride :one SELECT COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_title_generation_model_override'), '') :: text AS model_config_id @@ -25265,16 +25251,6 @@ func (q *sqlQuerier) UpsertChatSystemPrompt(ctx context.Context, value string) e return err } -const upsertChatTemplateAllowlist = `-- name: UpsertChatTemplateAllowlist :exec -INSERT INTO site_configs (key, value) VALUES ('agents_template_allowlist', $1) -ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_template_allowlist' -` - -func (q *sqlQuerier) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error { - _, err := q.db.ExecContext(ctx, upsertChatTemplateAllowlist, templateAllowlist) - return err -} - const upsertChatTitleGenerationModelOverride = `-- name: UpsertChatTitleGenerationModelOverride :exec INSERT INTO site_configs (key, value) VALUES ('agents_chat_title_generation_model_override', $1) ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_title_generation_model_override' diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index fbadbef223..5cc902baa7 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -313,12 +313,6 @@ SET value = CASE END WHERE site_configs.key = 'agents_chat_personal_model_overrides_enabled'; --- GetChatTemplateAllowlist returns the JSON-encoded template allowlist. --- Returns an empty string when no allowlist has been configured (all templates allowed). --- name: GetChatTemplateAllowlist :one -SELECT - COALESCE((SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'), '') :: text AS template_allowlist; - -- GetChatIncludeDefaultSystemPrompt preserves the legacy default -- for deployments created before the explicit include-default toggle. -- When the toggle is unset, a non-empty custom prompt implies false; @@ -360,10 +354,6 @@ SELECT '0s' )::text AS workspace_ttl; --- name: UpsertChatTemplateAllowlist :exec -INSERT INTO site_configs (key, value) VALUES ('agents_template_allowlist', @template_allowlist) -ON CONFLICT (key) DO UPDATE SET value = @template_allowlist WHERE site_configs.key = 'agents_template_allowlist'; - -- name: UpsertChatWorkspaceTTL :exec INSERT INTO site_configs (key, value) VALUES ('agents_workspace_ttl', @workspace_ttl::text) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 707f84ad03..13897ba2b4 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -42,7 +42,6 @@ import ( "github.com/coder/coder/v2/coderd/searchquery" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/util/ptr" - "github.com/coder/coder/v2/coderd/util/xjson" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/coderd/wsbuilder" "github.com/coder/coder/v2/coderd/x/agenthooks/dispatch" @@ -5593,140 +5592,6 @@ func (api *API) putChatAutoArchiveDays(rw http.ResponseWriter, r *http.Request) rw.WriteHeader(http.StatusNoContent) } -// EXPERIMENTAL: this endpoint is experimental and is subject to change. -// -//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler. -func (api *API) getChatTemplateAllowlist(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !api.Authorize(r, policy.ActionRead, rbac.ResourceDeploymentConfig) { - httpapi.ResourceNotFound(rw) - return - } - raw, err := api.Database.GetChatTemplateAllowlist(ctx) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching chat template allowlist.", - Detail: err.Error(), - }) - return - } - parsed, parseErr := xjson.ParseUUIDList(raw) - if parseErr != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Stored template allowlist is corrupt.", - Detail: parseErr.Error(), - }) - return - } - ids := make([]string, len(parsed)) - for i, id := range parsed { - ids[i] = id.String() - } - resp := codersdk.ChatTemplateAllowlist{ - TemplateIDs: ids, - } - httpapi.Write(ctx, rw, http.StatusOK, resp) -} - -// EXPERIMENTAL: this endpoint is experimental and is subject to change. -func (api *API) putChatTemplateAllowlist(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { - httpapi.ResourceNotFound(rw) - return - } - - var req codersdk.ChatTemplateAllowlist - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - // Validate all entries are valid UUIDs and deduplicate. - seen := make(map[string]struct{}, len(req.TemplateIDs)) - deduped := make([]string, 0, len(req.TemplateIDs)) - for _, id := range req.TemplateIDs { - parsed, err := uuid.Parse(id) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid template ID in allowlist.", - Detail: fmt.Sprintf("%q is not a valid UUID.", id), - }) - return - } - // Canonicalize to lowercase so deduplication is - // case-insensitive and stored values are consistent. - canonical := parsed.String() - if _, ok := seen[canonical]; !ok { - seen[canonical] = struct{}{} - deduped = append(deduped, canonical) - } - } - - // Convert to UUIDs for the database query. - parsedUUIDs := make([]uuid.UUID, len(deduped)) - for i, s := range deduped { - // Already validated above, safe to ignore error. - parsedUUIDs[i], _ = uuid.Parse(s) - } - - raw, err := json.Marshal(deduped) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error encoding template allowlist.", - Detail: err.Error(), - }) - return - } - - err = api.Database.InTx(func(tx database.Store) error { - // Verify all IDs refer to existing, non-deprecated templates - // in a single query. - if len(parsedUUIDs) > 0 { - found, err := tx.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ - IDs: parsedUUIDs, - Deprecated: sql.NullBool{ - Bool: false, - Valid: true, - }, - }) - if err != nil { - return xerrors.Errorf("fetch templates: %w", err) - } - if len(found) != len(parsedUUIDs) { - foundSet := make(map[uuid.UUID]struct{}, len(found)) - for _, t := range found { - foundSet[t.ID] = struct{}{} - } - var missing []string - for _, id := range parsedUUIDs { - if _, ok := foundSet[id]; !ok { - missing = append(missing, id.String()) - } - } - return xerrors.Errorf("templates not found or deprecated: %s", strings.Join(missing, ", ")) - } - } - return tx.UpsertChatTemplateAllowlist(ctx, string(raw)) - }, nil) - if err != nil { - // If the error mentions "not found or deprecated", it's a - // validation failure, not an internal error. - if strings.Contains(err.Error(), "not found or deprecated") { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "One or more templates not found or deprecated.", - Detail: err.Error(), - }) - return - } - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error updating chat template allowlist.", - Detail: err.Error(), - }) - return - } - rw.WriteHeader(http.StatusNoContent) -} - // EXPERIMENTAL: this endpoint is experimental and is subject to change. // //nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler. diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index 4531b34be7..21afca9210 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -14968,134 +14968,6 @@ func TestUserChatCompactionThresholds(t *testing.T) { }) } -//nolint:tparallel // Subtests share a single coderdtest instance and run sequentially. -func TestChatTemplateAllowlist(t *testing.T) { - t.Parallel() - - // Shared setup: one coderdtest instance with two real templates. - // Subtests that need valid template IDs use these. - client, store := newChatClientWithDatabase(t) - admin := coderdtest.CreateFirstUser(t, client.Client) - tmpl1 := dbgen.Template(t, store, database.Template{ - OrganizationID: admin.OrganizationID, - CreatedBy: admin.UserID, - }) - tmpl2 := dbgen.Template(t, store, database.Template{ - OrganizationID: admin.OrganizationID, - CreatedBy: admin.UserID, - }) - deprecatedTmpl := dbgen.Template(t, store, database.Template{ - OrganizationID: admin.OrganizationID, - CreatedBy: admin.UserID, - }) - //nolint:gocritic // Owner context needed to deprecate the template in test setup. - ownerRoles, err := rbac.RoleIdentifiers{rbac.RoleOwner()}.Expand() - require.NoError(t, err) - err = store.UpdateTemplateAccessControlByID(dbauthz.As(context.Background(), rbac.Subject{ - ID: "owner", - Roles: rbac.Roles(ownerRoles), - Scope: rbac.ExpandableScope(rbac.ScopeAll), - }), database.UpdateTemplateAccessControlByIDParams{ - ID: deprecatedTmpl.ID, - Deprecated: "this template is deprecated", - }) - require.NoError(t, err, "deprecate template") - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("ReturnsEmptyWhenUnset", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - resp, err := client.GetChatTemplateAllowlist(ctx) - require.NoError(t, err) - require.Empty(t, resp.TemplateIDs) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("AdminCanSet", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - ids := []string{tmpl1.ID.String(), tmpl2.ID.String()} - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: ids}) - require.NoError(t, err) - resp, err := client.GetChatTemplateAllowlist(ctx) - require.NoError(t, err) - require.ElementsMatch(t, ids, resp.TemplateIDs) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("AdminCanClear", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: []string{}}) - require.NoError(t, err) - resp, err := client.GetChatTemplateAllowlist(ctx) - require.NoError(t, err) - require.Empty(t, resp.TemplateIDs) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("NonAdminReadFails", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, admin.OrganizationID) - memberClient := codersdk.NewExperimentalClient(memberClientRaw) - _, err := memberClient.GetChatTemplateAllowlist(ctx) - requireSDKError(t, err, http.StatusNotFound) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("NonAdminWriteFails", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, admin.OrganizationID) - memberClient := codersdk.NewExperimentalClient(memberClientRaw) - // Uses a random UUID — hits 404 before template validation. - err := memberClient.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: []string{uuid.NewString()}}) - requireSDKError(t, err, http.StatusNotFound) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("UnauthenticatedFails", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - anonClient := codersdk.NewExperimentalClient(codersdk.New(client.URL)) - // Uses a random UUID — hits 401 before template validation. - err := anonClient.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: []string{uuid.NewString()}}) - requireSDKError(t, err, http.StatusUnauthorized) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("InvalidUUIDRejected", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: []string{"not-a-uuid"}}) - requireSDKError(t, err, http.StatusBadRequest) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("NonexistentTemplateRejected", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{TemplateIDs: []string{uuid.NewString()}}) - requireSDKError(t, err, http.StatusBadRequest) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("DeprecatedTemplateRejected", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{ - TemplateIDs: []string{deprecatedTmpl.ID.String()}, - }) - requireSDKError(t, err, http.StatusBadRequest) - }) - - //nolint:paralleltest // Sequential: subtests share a single coderdtest instance. - t.Run("DeduplicatesIDs", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitLong) - id := tmpl1.ID.String() - err := client.UpdateChatTemplateAllowlist(ctx, codersdk.ChatTemplateAllowlist{ - TemplateIDs: []string{id, id, id}, - }) - require.NoError(t, err) - resp, err := client.GetChatTemplateAllowlist(ctx) - require.NoError(t, err) - require.Len(t, resp.TemplateIDs, 1) - require.Equal(t, id, resp.TemplateIDs[0]) - }) -} - func TestGetChatsByWorkspace(t *testing.T) { t.Parallel() diff --git a/coderd/util/xjson/xjson.go b/coderd/util/xjson/xjson.go deleted file mode 100644 index 9d900e2305..0000000000 --- a/coderd/util/xjson/xjson.go +++ /dev/null @@ -1,35 +0,0 @@ -package xjson - -import ( - "encoding/json" - "strings" - - "github.com/google/uuid" - "golang.org/x/xerrors" -) - -// ParseUUIDList parses a JSON-encoded array of UUID strings -// (e.g. `["uuid1","uuid2"]`) and returns the corresponding -// slice of uuid.UUID values. An empty input (including -// whitespace-only) returns an empty (non-nil) slice. -func ParseUUIDList(raw string) ([]uuid.UUID, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return []uuid.UUID{}, nil - } - - var strs []string - if err := json.Unmarshal([]byte(raw), &strs); err != nil { - return nil, xerrors.Errorf("unmarshal uuid list: %w", err) - } - - ids := make([]uuid.UUID, 0, len(strs)) - for _, s := range strs { - id, err := uuid.Parse(s) - if err != nil { - return nil, xerrors.Errorf("parse uuid %q: %w", s, err) - } - ids = append(ids, id) - } - return ids, nil -} diff --git a/coderd/util/xjson/xjson_test.go b/coderd/util/xjson/xjson_test.go deleted file mode 100644 index 3a94811729..0000000000 --- a/coderd/util/xjson/xjson_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package xjson_test - -import ( - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/coderd/util/xjson" -) - -func TestParseUUIDList(t *testing.T) { - t.Parallel() - - a := uuid.MustParse("c7c6686d-a93c-4df2-bef9-5f837e9a33d5") - b := uuid.MustParse("8f3b3e0b-2c3f-46a5-a365-fd5b62bd8818") - - tests := []struct { - name string - input string - want []uuid.UUID - wantErr string - }{ - { - name: "EmptyString", - input: "", - want: []uuid.UUID{}, - }, - { - name: "JSONNull", - input: "null", - want: []uuid.UUID{}, - }, - { - name: "WhitespaceOnly", - input: " \n\t ", - want: []uuid.UUID{}, - }, - { - name: "ValidUUIDs", - input: `["c7c6686d-a93c-4df2-bef9-5f837e9a33d5","8f3b3e0b-2c3f-46a5-a365-fd5b62bd8818"]`, - want: []uuid.UUID{a, b}, - }, - { - name: "InvalidJSON", - input: "not json at all", - wantErr: "unmarshal uuid list", - }, - { - name: "InvalidUUID", - input: `["not-a-uuid"]`, - wantErr: "parse uuid", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := xjson.ParseUUIDList(tt.input) - if tt.wantErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.wantErr) - return - } - require.NoError(t, err) - require.NotNil(t, got) - require.Equal(t, tt.want, got) - }) - } -} diff --git a/codersdk/chats.go b/codersdk/chats.go index 0eb2e88499..df95d89a52 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -1205,13 +1205,6 @@ func ParseChatWorkspaceTTL(s string) (time.Duration, error) { return d, nil } -// ChatTemplateAllowlist is the request and response body for the -// chat template allowlist configuration endpoint. An empty list -// means all templates are allowed. -type ChatTemplateAllowlist struct { - TemplateIDs []string `json:"template_ids"` -} - // ChatProviderConfigSource describes how a provider entry is sourced. type ChatProviderConfigSource string @@ -2574,33 +2567,6 @@ func (c *ExperimentalClient) UpdateChatAutoArchiveDays(ctx context.Context, req return nil } -// GetChatTemplateAllowlist returns the deployment-wide chat template allowlist. -func (c *ExperimentalClient) GetChatTemplateAllowlist(ctx context.Context) (ChatTemplateAllowlist, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/template-allowlist", nil) - if err != nil { - return ChatTemplateAllowlist{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatTemplateAllowlist{}, ReadBodyAsError(res) - } - var resp ChatTemplateAllowlist - return resp, ReadBodyAsJSON(res, &resp) -} - -// UpdateChatTemplateAllowlist updates the deployment-wide chat template allowlist. -func (c *ExperimentalClient) UpdateChatTemplateAllowlist(ctx context.Context, req ChatTemplateAllowlist) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/template-allowlist", req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} - // UpdateUserChatCustomPrompt updates the user's custom chat prompt. func (c *ExperimentalClient) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) { res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 64a922b615..1d381a564d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -3695,14 +3695,6 @@ class ExperimentalApiMethods { return response.data; }; - getChatTemplateAllowlist = - async (): Promise => { - const response = await this.axios.get( - "/api/experimental/chats/config/template-allowlist", - ); - return response.data; - }; - updateChatWorkspaceTTL = async ( req: TypesGen.UpdateChatWorkspaceTTLRequest, ): Promise => { @@ -3759,15 +3751,6 @@ class ExperimentalApiMethods { ); }; - updateChatTemplateAllowlist = async ( - req: TypesGen.ChatTemplateAllowlist, - ): Promise => { - await this.axios.put( - "/api/experimental/chats/config/template-allowlist", - req, - ); - }; - getUserChatCustomPrompt = async (): Promise => { const response = await this.axios.get( diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0037abb3ca..ba98c91351 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3433,16 +3433,6 @@ export interface ChatSystemPromptResponse { readonly default_system_prompt: string; } -// From codersdk/chats.go -/** - * ChatTemplateAllowlist is the request and response body for the - * chat template allowlist configuration endpoint. An empty list - * means all templates are allowed. - */ -export interface ChatTemplateAllowlist { - readonly template_ids: readonly string[]; -} - // From codersdk/chats.go export interface ChatTextPart { readonly type: "text";