Compare commits

...

1 Commits

Author SHA1 Message Date
Kyle Carberry 56ff5dfded feat(coderd/database): add automations database schema and queries
Adds the database layer for the chat automations feature:

Schema (migration 000454):
- chat_automations table with status, rate limits, instructions, MCP config
- chat_automation_triggers table (webhook + cron types)
- chat_automation_events table for audit trail
- Indexes for performance and unique constraint on (owner, org, name)
- automation_id FK on chats table

SQL queries:
- chatautomations.sql: CRUD with authorize_filter support
- chatautomationtriggers.sql: CRUD + GetActiveChatAutomationCronTriggers JOIN query
- chatautomationevents.sql: insert, list, count windows, purge

RBAC:
- chat_automation resource with CRUD actions
- dbauthz wrappers for all 18 query methods

Also adds LockIDChatAutomationCron advisory lock constant.
2026-04-01 14:10:38 +00:00
38 changed files with 3402 additions and 57 deletions
+12
View File
@@ -13445,6 +13445,11 @@ const docTemplate = `{
"chat:delete",
"chat:read",
"chat:update",
"chat_automation:*",
"chat_automation:create",
"chat_automation:delete",
"chat_automation:read",
"chat_automation:update",
"coder:all",
"coder:apikeys.manage_self",
"coder:application_connect",
@@ -13654,6 +13659,11 @@ const docTemplate = `{
"APIKeyScopeChatDelete",
"APIKeyScopeChatRead",
"APIKeyScopeChatUpdate",
"APIKeyScopeChatAutomationAll",
"APIKeyScopeChatAutomationCreate",
"APIKeyScopeChatAutomationDelete",
"APIKeyScopeChatAutomationRead",
"APIKeyScopeChatAutomationUpdate",
"APIKeyScopeCoderAll",
"APIKeyScopeCoderApikeysManageSelf",
"APIKeyScopeCoderApplicationConnect",
@@ -19038,6 +19048,7 @@ const docTemplate = `{
"audit_log",
"boundary_usage",
"chat",
"chat_automation",
"connection_log",
"crypto_key",
"debug_info",
@@ -19084,6 +19095,7 @@ const docTemplate = `{
"ResourceAuditLog",
"ResourceBoundaryUsage",
"ResourceChat",
"ResourceChatAutomation",
"ResourceConnectionLog",
"ResourceCryptoKey",
"ResourceDebugInfo",
+12
View File
@@ -12015,6 +12015,11 @@
"chat:delete",
"chat:read",
"chat:update",
"chat_automation:*",
"chat_automation:create",
"chat_automation:delete",
"chat_automation:read",
"chat_automation:update",
"coder:all",
"coder:apikeys.manage_self",
"coder:application_connect",
@@ -12224,6 +12229,11 @@
"APIKeyScopeChatDelete",
"APIKeyScopeChatRead",
"APIKeyScopeChatUpdate",
"APIKeyScopeChatAutomationAll",
"APIKeyScopeChatAutomationCreate",
"APIKeyScopeChatAutomationDelete",
"APIKeyScopeChatAutomationRead",
"APIKeyScopeChatAutomationUpdate",
"APIKeyScopeCoderAll",
"APIKeyScopeCoderApikeysManageSelf",
"APIKeyScopeCoderApplicationConnect",
@@ -17410,6 +17420,7 @@
"audit_log",
"boundary_usage",
"chat",
"chat_automation",
"connection_log",
"crypto_key",
"debug_info",
@@ -17456,6 +17467,7 @@
"ResourceAuditLog",
"ResourceBoundaryUsage",
"ResourceChat",
"ResourceChatAutomation",
"ResourceConnectionLog",
"ResourceCryptoKey",
"ResourceDebugInfo",
+5
View File
@@ -7,6 +7,11 @@ type CheckConstraint string
// CheckConstraint enums.
const (
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
CheckChatAutomationEventsChatExclusivity CheckConstraint = "chat_automation_events_chat_exclusivity" // chat_automation_events
CheckChatAutomationTriggersCronFields CheckConstraint = "chat_automation_triggers_cron_fields" // chat_automation_triggers
CheckChatAutomationTriggersWebhookFields CheckConstraint = "chat_automation_triggers_webhook_fields" // chat_automation_triggers
CheckChatAutomationsMaxChatCreatesPerHourCheck CheckConstraint = "chat_automations_max_chat_creates_per_hour_check" // chat_automations
CheckChatAutomationsMaxMessagesPerHourCheck CheckConstraint = "chat_automations_max_messages_per_hour_check" // chat_automations
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
CheckChatProvidersProviderCheck CheckConstraint = "chat_providers_provider_check" // chat_providers
+212
View File
@@ -1694,6 +1694,13 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
return q.db.CleanTailnetTunnels(ctx)
}
func (q *querier) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChatAutomation); err != nil {
return err
}
return q.db.CleanupDeletedMCPServerIDsFromChatAutomations(ctx)
}
func (q *querier) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
return err
@@ -1731,6 +1738,28 @@ func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLog
return q.db.CountAuthorizedAuditLogs(ctx, arg, prep)
}
func (q *querier) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
return 0, err
}
return q.db.CountChatAutomationChatCreatesInWindow(ctx, arg)
}
func (q *querier) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
if err != nil {
return 0, err
}
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
return 0, err
}
return q.db.CountChatAutomationMessagesInWindow(ctx, arg)
}
func (q *querier) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
// Just like the actual query, shortcut if the user is an owner.
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog)
@@ -1842,6 +1871,28 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u
return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
}
func (q *querier) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
return deleteQ(q.log, q.auth, q.db.GetChatAutomationByID, q.db.DeleteChatAutomationByID)(ctx, id)
}
// Triggers are sub-resources of an automation. Deleting a trigger
// is a configuration change, so we authorize ActionUpdate on the
// parent rather than ActionDelete.
func (q *querier) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, id)
if err != nil {
return err
}
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return err
}
return q.db.DeleteChatAutomationTriggerByID(ctx, id)
}
func (q *querier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
@@ -2386,6 +2437,16 @@ func (q *querier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
return q.db.GetActiveAISeatCount(ctx)
}
// GetActiveChatAutomationCronTriggers is a system-level query used by
// the cron scheduler. It requires read permission on all automations
// (admin gate) because it fetches triggers across all orgs and owners.
func (q *querier) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChatAutomation.All()); err != nil {
return nil, err
}
return q.db.GetActiveChatAutomationCronTriggers(ctx)
}
func (q *querier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil {
return nil, err
@@ -2477,6 +2538,64 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
return q.db.GetAuthorizationUserRoles(ctx, userID)
}
func (q *querier) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
return fetch(q.log, q.auth, q.db.GetChatAutomationByID)(ctx, id)
}
func (q *querier) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
return nil, err
}
return q.db.GetChatAutomationEventsByAutomationID(ctx, arg)
}
func (q *querier) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, id)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
return database.ChatAutomationTrigger{}, err
}
return trigger, nil
}
func (q *querier) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
automation, err := q.db.GetChatAutomationByID(ctx, automationID)
if err != nil {
return nil, err
}
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
return nil, err
}
return q.db.GetChatAutomationTriggersByAutomationID(ctx, automationID)
}
func (q *querier) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
// Shortcut if the caller has broad read access (e.g. site admins
// / owners). The SQL filter is noticeable, so skip it when we
// can.
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChatAutomation.All())
if err == nil {
return q.db.GetChatAutomations(ctx, arg)
}
// Fall back to SQL-level row filtering for normal users.
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceChatAutomation.Type)
if err != nil {
return nil, xerrors.Errorf("prepare chat automation SQL filter: %w", err)
}
return q.db.GetAuthorizedChatAutomations(ctx, arg, prep)
}
func (q *querier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
return fetch(q.log, q.auth, q.db.GetChatByID)(ctx, id)
}
@@ -4772,6 +4891,36 @@ func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams)
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg)
}
func (q *querier) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
return insert(q.log, q.auth, rbac.ResourceChatAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChatAutomation)(ctx, arg)
}
// Events are append-only records produced by the system when
// triggers fire. We authorize ActionUpdate on the parent
// automation because inserting an event is a side-effect of
// processing the automation, not an independent create action.
func (q *querier) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
if err != nil {
return database.ChatAutomationEvent{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return database.ChatAutomationEvent{}, err
}
return q.db.InsertChatAutomationEvent(ctx, arg)
}
func (q *querier) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return database.ChatAutomationTrigger{}, err
}
return q.db.InsertChatAutomationTrigger(ctx, arg)
}
func (q *querier) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
// Authorize create on chat resource scoped to the owner and org.
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChatFile)(ctx, arg)
@@ -5569,6 +5718,13 @@ func (q *querier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (d
return q.db.PopNextQueuedMessage(ctx, chatID)
}
func (q *querier) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceChatAutomation.All()); err != nil {
return 0, err
}
return q.db.PurgeOldChatAutomationEvents(ctx, arg)
}
func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
template, err := q.db.GetTemplateByID(ctx, templateID)
if err != nil {
@@ -5715,6 +5871,58 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
}
func (q *querier) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
fetchFunc := func(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
return q.db.GetChatAutomationByID(ctx, arg.ID)
}
return updateWithReturn(q.log, q.auth, fetchFunc, q.db.UpdateChatAutomation)(ctx, arg)
}
func (q *querier) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return database.ChatAutomationTrigger{}, err
}
return q.db.UpdateChatAutomationTrigger(ctx, arg)
}
func (q *querier) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
if err != nil {
return err
}
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return err
}
return q.db.UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg)
}
func (q *querier) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
return database.ChatAutomationTrigger{}, err
}
return q.db.UpdateChatAutomationTriggerWebhookSecret(ctx, arg)
}
func (q *querier) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
@@ -7352,3 +7560,7 @@ func (q *querier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg
func (q *querier) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, _ rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
return q.GetChats(ctx, arg)
}
func (q *querier) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, _ rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
return q.GetChatAutomations(ctx, arg)
}
+224
View File
@@ -1121,6 +1121,10 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().CleanupDeletedMCPServerIDsFromChats(gomock.Any()).Return(nil).AnyTimes()
check.Args().Asserts(rbac.ResourceChat, policy.ActionUpdate)
}))
s.Run("CleanupDeletedMCPServerIDsFromChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().CleanupDeletedMCPServerIDsFromChatAutomations(gomock.Any()).Return(nil).AnyTimes()
check.Args().Asserts(rbac.ResourceChatAutomation, policy.ActionUpdate)
}))
s.Run("DeleteMCPServerConfigByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
id := uuid.New()
dbm.EXPECT().DeleteMCPServerConfigByID(gomock.Any(), id).Return(nil).AnyTimes()
@@ -1250,6 +1254,226 @@ func (s *MethodTestSuite) TestChats() {
}))
}
func (s *MethodTestSuite) TestChatAutomations() {
s.Run("CountChatAutomationChatCreatesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.CountChatAutomationChatCreatesInWindowParams{
AutomationID: automation.ID,
WindowStart: dbtime.Now().Add(-time.Hour),
}
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().CountChatAutomationChatCreatesInWindow(gomock.Any(), arg).Return(int64(3), nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(3))
}))
s.Run("CountChatAutomationMessagesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.CountChatAutomationMessagesInWindowParams{
AutomationID: automation.ID,
WindowStart: dbtime.Now().Add(-time.Hour),
}
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().CountChatAutomationMessagesInWindow(gomock.Any(), arg).Return(int64(5), nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(5))
}))
s.Run("DeleteChatAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().DeleteChatAutomationByID(gomock.Any(), automation.ID).Return(nil).AnyTimes()
check.Args(automation.ID).Asserts(automation, policy.ActionDelete).Returns()
}))
s.Run("DeleteChatAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeWebhook,
})
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().DeleteChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(nil).AnyTimes()
check.Args(trigger.ID).Asserts(automation, policy.ActionUpdate).Returns()
}))
s.Run("GetActiveChatAutomationCronTriggers", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
rows := []database.GetActiveChatAutomationCronTriggersRow{}
dbm.EXPECT().GetActiveChatAutomationCronTriggers(gomock.Any()).Return(rows, nil).AnyTimes()
check.Args().Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead).Returns(rows)
}))
s.Run("GetChatAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns(automation)
}))
s.Run("GetChatAutomationEventsByAutomationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.GetChatAutomationEventsByAutomationIDParams{
AutomationID: automation.ID,
}
events := []database.ChatAutomationEvent{}
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationEventsByAutomationID(gomock.Any(), arg).Return(events, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(events)
}))
s.Run("GetChatAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeWebhook,
})
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
check.Args(trigger.ID).Asserts(automation, policy.ActionRead).Returns(trigger)
}))
s.Run("GetChatAutomationTriggersByAutomationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
triggers := []database.ChatAutomationTrigger{}
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationTriggersByAutomationID(gomock.Any(), automation.ID).Return(triggers, nil).AnyTimes()
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns(triggers)
}))
s.Run("GetChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
params := database.GetChatAutomationsParams{}
dbm.EXPECT().GetChatAutomations(gomock.Any(), params).Return([]database.ChatAutomation{}, nil).AnyTimes()
dbm.EXPECT().GetAuthorizedChatAutomations(gomock.Any(), params, gomock.Any()).Return([]database.ChatAutomation{}, nil).AnyTimes()
check.Args(params).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead).WithNotAuthorized("nil")
}))
s.Run("GetAuthorizedChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
params := database.GetChatAutomationsParams{}
dbm.EXPECT().GetAuthorizedChatAutomations(gomock.Any(), params, gomock.Any()).Return([]database.ChatAutomation{}, nil).AnyTimes()
dbm.EXPECT().GetChatAutomations(gomock.Any(), params).Return([]database.ChatAutomation{}, nil).AnyTimes()
check.Args(params, emptyPreparedAuthorized{}).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead)
}))
s.Run("InsertChatAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.InsertChatAutomationParams{
ID: uuid.New(),
OwnerID: uuid.New(),
OrganizationID: uuid.New(),
Name: "test-automation",
Description: "test description",
Instructions: "test instructions",
Status: database.ChatAutomationStatusActive,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
}
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{
ID: arg.ID,
OwnerID: arg.OwnerID,
OrganizationID: arg.OrganizationID,
Status: arg.Status,
})
dbm.EXPECT().InsertChatAutomation(gomock.Any(), arg).Return(automation, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChatAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), policy.ActionCreate).Returns(automation)
}))
s.Run("InsertChatAutomationEvent", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.InsertChatAutomationEventParams{
ID: uuid.New(),
AutomationID: automation.ID,
ReceivedAt: dbtime.Now(),
Payload: json.RawMessage(`{}`),
Status: database.ChatAutomationEventStatusFiltered,
}
event := testutil.Fake(s.T(), faker, database.ChatAutomationEvent{
ID: arg.ID,
AutomationID: automation.ID,
Status: arg.Status,
})
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().InsertChatAutomationEvent(gomock.Any(), arg).Return(event, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(event)
}))
s.Run("InsertChatAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.InsertChatAutomationTriggerParams{
ID: uuid.New(),
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeWebhook,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
}
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
ID: arg.ID,
AutomationID: automation.ID,
Type: arg.Type,
})
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().InsertChatAutomationTrigger(gomock.Any(), arg).Return(trigger, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(trigger)
}))
s.Run("PurgeOldChatAutomationEvents", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.PurgeOldChatAutomationEventsParams{
Before: dbtime.Now().Add(-7 * 24 * time.Hour),
LimitCount: 1000,
}
dbm.EXPECT().PurgeOldChatAutomationEvents(gomock.Any(), arg).Return(int64(5), nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionDelete).Returns(int64(5))
}))
s.Run("UpdateChatAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
arg := database.UpdateChatAutomationParams{
ID: automation.ID,
Name: "updated-name",
Description: "updated description",
Status: database.ChatAutomationStatusActive,
UpdatedAt: dbtime.Now(),
}
updated := automation
updated.Name = arg.Name
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().UpdateChatAutomation(gomock.Any(), arg).Return(updated, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
}))
s.Run("UpdateChatAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeCron,
})
arg := database.UpdateChatAutomationTriggerParams{
ID: trigger.ID,
UpdatedAt: dbtime.Now(),
}
updated := trigger
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().UpdateChatAutomationTrigger(gomock.Any(), arg).Return(updated, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
}))
s.Run("UpdateChatAutomationTriggerLastTriggeredAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeCron,
})
arg := database.UpdateChatAutomationTriggerLastTriggeredAtParams{
ID: trigger.ID,
LastTriggeredAt: dbtime.Now(),
}
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().UpdateChatAutomationTriggerLastTriggeredAt(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns()
}))
s.Run("UpdateChatAutomationTriggerWebhookSecret", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeWebhook,
})
arg := database.UpdateChatAutomationTriggerWebhookSecretParams{
ID: trigger.ID,
UpdatedAt: dbtime.Now(),
WebhookSecret: sql.NullString{
String: "new-secret",
Valid: true,
},
}
updated := trigger
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
dbm.EXPECT().UpdateChatAutomationTriggerWebhookSecret(gomock.Any(), arg).Return(updated, nil).AnyTimes()
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
}))
}
func (s *MethodTestSuite) TestFile() {
s.Run("GetFileByHashAndCreator", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
f := testutil.Fake(s.T(), faker, database.File{})
+160
View File
@@ -264,6 +264,14 @@ func (m queryMetricsStore) CleanTailnetTunnels(ctx context.Context) error {
return r0
}
func (m queryMetricsStore) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
start := time.Now()
r0 := m.s.CleanupDeletedMCPServerIDsFromChatAutomations(ctx)
m.queryLatencies.WithLabelValues("CleanupDeletedMCPServerIDsFromChatAutomations").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CleanupDeletedMCPServerIDsFromChatAutomations").Inc()
return r0
}
func (m queryMetricsStore) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
start := time.Now()
r0 := m.s.CleanupDeletedMCPServerIDsFromChats(ctx)
@@ -296,6 +304,22 @@ func (m queryMetricsStore) CountAuditLogs(ctx context.Context, arg database.Coun
return r0, r1
}
func (m queryMetricsStore) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountChatAutomationChatCreatesInWindow(ctx, arg)
m.queryLatencies.WithLabelValues("CountChatAutomationChatCreatesInWindow").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountChatAutomationChatCreatesInWindow").Inc()
return r0, r1
}
func (m queryMetricsStore) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountChatAutomationMessagesInWindow(ctx, arg)
m.queryLatencies.WithLabelValues("CountChatAutomationMessagesInWindow").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountChatAutomationMessagesInWindow").Inc()
return r0, r1
}
func (m queryMetricsStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountConnectionLogs(ctx, arg)
@@ -400,6 +424,22 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C
return r0
}
func (m queryMetricsStore) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteChatAutomationByID(ctx, id)
m.queryLatencies.WithLabelValues("DeleteChatAutomationByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatAutomationByID").Inc()
return r0
}
func (m queryMetricsStore) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteChatAutomationTriggerByID(ctx, id)
m.queryLatencies.WithLabelValues("DeleteChatAutomationTriggerByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatAutomationTriggerByID").Inc()
return r0
}
func (m queryMetricsStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteChatModelConfigByID(ctx, id)
@@ -936,6 +976,14 @@ func (m queryMetricsStore) GetActiveAISeatCount(ctx context.Context) (int64, err
return r0, r1
}
func (m queryMetricsStore) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
start := time.Now()
r0, r1 := m.s.GetActiveChatAutomationCronTriggers(ctx)
m.queryLatencies.WithLabelValues("GetActiveChatAutomationCronTriggers").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetActiveChatAutomationCronTriggers").Inc()
return r0, r1
}
func (m queryMetricsStore) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
start := time.Now()
r0, r1 := m.s.GetActivePresetPrebuildSchedules(ctx)
@@ -1032,6 +1080,46 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID
return r0, r1
}
func (m queryMetricsStore) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
start := time.Now()
r0, r1 := m.s.GetChatAutomationByID(ctx, id)
m.queryLatencies.WithLabelValues("GetChatAutomationByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
start := time.Now()
r0, r1 := m.s.GetChatAutomationEventsByAutomationID(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatAutomationEventsByAutomationID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationEventsByAutomationID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
start := time.Now()
r0, r1 := m.s.GetChatAutomationTriggerByID(ctx, id)
m.queryLatencies.WithLabelValues("GetChatAutomationTriggerByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationTriggerByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
start := time.Now()
r0, r1 := m.s.GetChatAutomationTriggersByAutomationID(ctx, automationID)
m.queryLatencies.WithLabelValues("GetChatAutomationTriggersByAutomationID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationTriggersByAutomationID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
start := time.Now()
r0, r1 := m.s.GetChatAutomations(ctx, arg)
m.queryLatencies.WithLabelValues("GetChatAutomations").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomations").Inc()
return r0, r1
}
func (m queryMetricsStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.GetChatByID(ctx, id)
@@ -3224,6 +3312,30 @@ func (m queryMetricsStore) InsertChat(ctx context.Context, arg database.InsertCh
return r0, r1
}
func (m queryMetricsStore) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
start := time.Now()
r0, r1 := m.s.InsertChatAutomation(ctx, arg)
m.queryLatencies.WithLabelValues("InsertChatAutomation").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomation").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
start := time.Now()
r0, r1 := m.s.InsertChatAutomationEvent(ctx, arg)
m.queryLatencies.WithLabelValues("InsertChatAutomationEvent").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomationEvent").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
start := time.Now()
r0, r1 := m.s.InsertChatAutomationTrigger(ctx, arg)
m.queryLatencies.WithLabelValues("InsertChatAutomationTrigger").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomationTrigger").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
start := time.Now()
r0, r1 := m.s.InsertChatFile(ctx, arg)
@@ -3952,6 +4064,14 @@ func (m queryMetricsStore) PopNextQueuedMessage(ctx context.Context, chatID uuid
return r0, r1
}
func (m queryMetricsStore) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.PurgeOldChatAutomationEvents(ctx, arg)
m.queryLatencies.WithLabelValues("PurgeOldChatAutomationEvents").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "PurgeOldChatAutomationEvents").Inc()
return r0, r1
}
func (m queryMetricsStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
start := time.Now()
r0 := m.s.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID)
@@ -4080,6 +4200,38 @@ func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.Up
return r0
}
func (m queryMetricsStore) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatAutomation(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatAutomation").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomation").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatAutomationTrigger(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatAutomationTrigger").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTrigger").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
start := time.Now()
r0 := m.s.UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatAutomationTriggerLastTriggeredAt").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTriggerLastTriggeredAt").Inc()
return r0
}
func (m queryMetricsStore) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatAutomationTriggerWebhookSecret(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateChatAutomationTriggerWebhookSecret").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTriggerWebhookSecret").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
start := time.Now()
r0, r1 := m.s.UpdateChatBuildAgentBinding(ctx, arg)
@@ -5351,3 +5503,11 @@ func (m queryMetricsStore) GetAuthorizedChats(ctx context.Context, arg database.
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChats").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
start := time.Now()
r0, r1 := m.s.GetAuthorizedChatAutomations(ctx, arg, prepared)
m.queryLatencies.WithLabelValues("GetAuthorizedChatAutomations").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChatAutomations").Inc()
return r0, r1
}
+296
View File
@@ -335,6 +335,20 @@ func (mr *MockStoreMockRecorder) CleanTailnetTunnels(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetTunnels", reflect.TypeOf((*MockStore)(nil).CleanTailnetTunnels), ctx)
}
// CleanupDeletedMCPServerIDsFromChatAutomations mocks base method.
func (m *MockStore) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CleanupDeletedMCPServerIDsFromChatAutomations", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// CleanupDeletedMCPServerIDsFromChatAutomations indicates an expected call of CleanupDeletedMCPServerIDsFromChatAutomations.
func (mr *MockStoreMockRecorder) CleanupDeletedMCPServerIDsFromChatAutomations(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupDeletedMCPServerIDsFromChatAutomations", reflect.TypeOf((*MockStore)(nil).CleanupDeletedMCPServerIDsFromChatAutomations), ctx)
}
// CleanupDeletedMCPServerIDsFromChats mocks base method.
func (m *MockStore) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
m.ctrl.T.Helper()
@@ -454,6 +468,36 @@ func (mr *MockStoreMockRecorder) CountAuthorizedConnectionLogs(ctx, arg, prepare
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAuthorizedConnectionLogs", reflect.TypeOf((*MockStore)(nil).CountAuthorizedConnectionLogs), ctx, arg, prepared)
}
// CountChatAutomationChatCreatesInWindow mocks base method.
func (m *MockStore) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountChatAutomationChatCreatesInWindow", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountChatAutomationChatCreatesInWindow indicates an expected call of CountChatAutomationChatCreatesInWindow.
func (mr *MockStoreMockRecorder) CountChatAutomationChatCreatesInWindow(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountChatAutomationChatCreatesInWindow", reflect.TypeOf((*MockStore)(nil).CountChatAutomationChatCreatesInWindow), ctx, arg)
}
// CountChatAutomationMessagesInWindow mocks base method.
func (m *MockStore) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountChatAutomationMessagesInWindow", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountChatAutomationMessagesInWindow indicates an expected call of CountChatAutomationMessagesInWindow.
func (mr *MockStoreMockRecorder) CountChatAutomationMessagesInWindow(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountChatAutomationMessagesInWindow", reflect.TypeOf((*MockStore)(nil).CountChatAutomationMessagesInWindow), ctx, arg)
}
// CountConnectionLogs mocks base method.
func (m *MockStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
m.ctrl.T.Helper()
@@ -643,6 +687,34 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID)
}
// DeleteChatAutomationByID mocks base method.
func (m *MockStore) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteChatAutomationByID", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteChatAutomationByID indicates an expected call of DeleteChatAutomationByID.
func (mr *MockStoreMockRecorder) DeleteChatAutomationByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatAutomationByID", reflect.TypeOf((*MockStore)(nil).DeleteChatAutomationByID), ctx, id)
}
// DeleteChatAutomationTriggerByID mocks base method.
func (m *MockStore) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteChatAutomationTriggerByID", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteChatAutomationTriggerByID indicates an expected call of DeleteChatAutomationTriggerByID.
func (mr *MockStoreMockRecorder) DeleteChatAutomationTriggerByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).DeleteChatAutomationTriggerByID), ctx, id)
}
// DeleteChatModelConfigByID mocks base method.
func (m *MockStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
@@ -1609,6 +1681,21 @@ func (mr *MockStoreMockRecorder) GetActiveAISeatCount(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveAISeatCount", reflect.TypeOf((*MockStore)(nil).GetActiveAISeatCount), ctx)
}
// GetActiveChatAutomationCronTriggers mocks base method.
func (m *MockStore) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetActiveChatAutomationCronTriggers", ctx)
ret0, _ := ret[0].([]database.GetActiveChatAutomationCronTriggersRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetActiveChatAutomationCronTriggers indicates an expected call of GetActiveChatAutomationCronTriggers.
func (mr *MockStoreMockRecorder) GetActiveChatAutomationCronTriggers(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveChatAutomationCronTriggers", reflect.TypeOf((*MockStore)(nil).GetActiveChatAutomationCronTriggers), ctx)
}
// GetActivePresetPrebuildSchedules mocks base method.
func (m *MockStore) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
m.ctrl.T.Helper()
@@ -1804,6 +1891,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedAuditLogsOffset(ctx, arg, prepared
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedAuditLogsOffset", reflect.TypeOf((*MockStore)(nil).GetAuthorizedAuditLogsOffset), ctx, arg, prepared)
}
// GetAuthorizedChatAutomations mocks base method.
func (m *MockStore) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthorizedChatAutomations", ctx, arg, prepared)
ret0, _ := ret[0].([]database.ChatAutomation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAuthorizedChatAutomations indicates an expected call of GetAuthorizedChatAutomations.
func (mr *MockStoreMockRecorder) GetAuthorizedChatAutomations(ctx, arg, prepared any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedChatAutomations", reflect.TypeOf((*MockStore)(nil).GetAuthorizedChatAutomations), ctx, arg, prepared)
}
// GetAuthorizedChats mocks base method.
func (m *MockStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
m.ctrl.T.Helper()
@@ -1894,6 +1996,81 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared)
}
// GetChatAutomationByID mocks base method.
func (m *MockStore) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatAutomationByID", ctx, id)
ret0, _ := ret[0].(database.ChatAutomation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatAutomationByID indicates an expected call of GetChatAutomationByID.
func (mr *MockStoreMockRecorder) GetChatAutomationByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationByID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationByID), ctx, id)
}
// GetChatAutomationEventsByAutomationID mocks base method.
func (m *MockStore) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatAutomationEventsByAutomationID", ctx, arg)
ret0, _ := ret[0].([]database.ChatAutomationEvent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatAutomationEventsByAutomationID indicates an expected call of GetChatAutomationEventsByAutomationID.
func (mr *MockStoreMockRecorder) GetChatAutomationEventsByAutomationID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationEventsByAutomationID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationEventsByAutomationID), ctx, arg)
}
// GetChatAutomationTriggerByID mocks base method.
func (m *MockStore) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatAutomationTriggerByID", ctx, id)
ret0, _ := ret[0].(database.ChatAutomationTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatAutomationTriggerByID indicates an expected call of GetChatAutomationTriggerByID.
func (mr *MockStoreMockRecorder) GetChatAutomationTriggerByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationTriggerByID), ctx, id)
}
// GetChatAutomationTriggersByAutomationID mocks base method.
func (m *MockStore) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatAutomationTriggersByAutomationID", ctx, automationID)
ret0, _ := ret[0].([]database.ChatAutomationTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatAutomationTriggersByAutomationID indicates an expected call of GetChatAutomationTriggersByAutomationID.
func (mr *MockStoreMockRecorder) GetChatAutomationTriggersByAutomationID(ctx, automationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationTriggersByAutomationID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationTriggersByAutomationID), ctx, automationID)
}
// GetChatAutomations mocks base method.
func (m *MockStore) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChatAutomations", ctx, arg)
ret0, _ := ret[0].([]database.ChatAutomation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChatAutomations indicates an expected call of GetChatAutomations.
func (mr *MockStoreMockRecorder) GetChatAutomations(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomations", reflect.TypeOf((*MockStore)(nil).GetChatAutomations), ctx, arg)
}
// GetChatByID mocks base method.
func (m *MockStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
m.ctrl.T.Helper()
@@ -6048,6 +6225,51 @@ func (mr *MockStoreMockRecorder) InsertChat(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChat", reflect.TypeOf((*MockStore)(nil).InsertChat), ctx, arg)
}
// InsertChatAutomation mocks base method.
func (m *MockStore) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertChatAutomation", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertChatAutomation indicates an expected call of InsertChatAutomation.
func (mr *MockStoreMockRecorder) InsertChatAutomation(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomation", reflect.TypeOf((*MockStore)(nil).InsertChatAutomation), ctx, arg)
}
// InsertChatAutomationEvent mocks base method.
func (m *MockStore) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertChatAutomationEvent", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomationEvent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertChatAutomationEvent indicates an expected call of InsertChatAutomationEvent.
func (mr *MockStoreMockRecorder) InsertChatAutomationEvent(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomationEvent", reflect.TypeOf((*MockStore)(nil).InsertChatAutomationEvent), ctx, arg)
}
// InsertChatAutomationTrigger mocks base method.
func (m *MockStore) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertChatAutomationTrigger", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomationTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertChatAutomationTrigger indicates an expected call of InsertChatAutomationTrigger.
func (mr *MockStoreMockRecorder) InsertChatAutomationTrigger(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomationTrigger", reflect.TypeOf((*MockStore)(nil).InsertChatAutomationTrigger), ctx, arg)
}
// InsertChatFile mocks base method.
func (m *MockStore) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
m.ctrl.T.Helper()
@@ -7501,6 +7723,21 @@ func (mr *MockStoreMockRecorder) PopNextQueuedMessage(ctx, chatID any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopNextQueuedMessage", reflect.TypeOf((*MockStore)(nil).PopNextQueuedMessage), ctx, chatID)
}
// PurgeOldChatAutomationEvents mocks base method.
func (m *MockStore) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PurgeOldChatAutomationEvents", ctx, arg)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PurgeOldChatAutomationEvents indicates an expected call of PurgeOldChatAutomationEvents.
func (mr *MockStoreMockRecorder) PurgeOldChatAutomationEvents(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgeOldChatAutomationEvents", reflect.TypeOf((*MockStore)(nil).PurgeOldChatAutomationEvents), ctx, arg)
}
// ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate mocks base method.
func (m *MockStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
m.ctrl.T.Helper()
@@ -7732,6 +7969,65 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), ctx, arg)
}
// UpdateChatAutomation mocks base method.
func (m *MockStore) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatAutomation", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateChatAutomation indicates an expected call of UpdateChatAutomation.
func (mr *MockStoreMockRecorder) UpdateChatAutomation(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomation", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomation), ctx, arg)
}
// UpdateChatAutomationTrigger mocks base method.
func (m *MockStore) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatAutomationTrigger", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomationTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateChatAutomationTrigger indicates an expected call of UpdateChatAutomationTrigger.
func (mr *MockStoreMockRecorder) UpdateChatAutomationTrigger(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTrigger", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTrigger), ctx, arg)
}
// UpdateChatAutomationTriggerLastTriggeredAt mocks base method.
func (m *MockStore) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatAutomationTriggerLastTriggeredAt", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateChatAutomationTriggerLastTriggeredAt indicates an expected call of UpdateChatAutomationTriggerLastTriggeredAt.
func (mr *MockStoreMockRecorder) UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTriggerLastTriggeredAt", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTriggerLastTriggeredAt), ctx, arg)
}
// UpdateChatAutomationTriggerWebhookSecret mocks base method.
func (m *MockStore) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateChatAutomationTriggerWebhookSecret", ctx, arg)
ret0, _ := ret[0].(database.ChatAutomationTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateChatAutomationTriggerWebhookSecret indicates an expected call of UpdateChatAutomationTriggerWebhookSecret.
func (mr *MockStoreMockRecorder) UpdateChatAutomationTriggerWebhookSecret(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTriggerWebhookSecret", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTriggerWebhookSecret), ctx, arg)
}
// UpdateChatBuildAgentBinding mocks base method.
func (m *MockStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
m.ctrl.T.Helper()
+187 -2
View File
@@ -220,7 +220,12 @@ CREATE TYPE api_key_scope AS ENUM (
'chat:read',
'chat:update',
'chat:delete',
'chat:*'
'chat:*',
'chat_automation:create',
'chat_automation:read',
'chat_automation:update',
'chat_automation:delete',
'chat_automation:*'
);
CREATE TYPE app_sharing_level AS ENUM (
@@ -270,6 +275,32 @@ CREATE TYPE build_reason AS ENUM (
'task_resume'
);
CREATE TYPE chat_automation_event_status AS ENUM (
'filtered',
'preview',
'created',
'continued',
'rate_limited',
'error'
);
COMMENT ON TYPE chat_automation_event_status IS 'Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.';
CREATE TYPE chat_automation_status AS ENUM (
'disabled',
'preview',
'active'
);
COMMENT ON TYPE chat_automation_status IS 'Lifecycle state of a chat automation: disabled, preview, or active.';
CREATE TYPE chat_automation_trigger_type AS ENUM (
'webhook',
'cron'
);
COMMENT ON TYPE chat_automation_trigger_type IS 'Discriminator for chat automation triggers: webhook or cron.';
CREATE TYPE chat_message_role AS ENUM (
'system',
'user',
@@ -1238,6 +1269,104 @@ COMMENT ON COLUMN boundary_usage_stats.window_start IS 'Start of the time window
COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.';
CREATE TABLE chat_automation_events (
id uuid NOT NULL,
automation_id uuid NOT NULL,
trigger_id uuid,
received_at timestamp with time zone NOT NULL,
payload jsonb NOT NULL,
filter_matched boolean NOT NULL,
resolved_labels jsonb,
matched_chat_id uuid,
created_chat_id uuid,
status chat_automation_event_status NOT NULL,
error text,
CONSTRAINT chat_automation_events_chat_exclusivity CHECK (((matched_chat_id IS NULL) OR (created_chat_id IS NULL)))
);
COMMENT ON TABLE chat_automation_events IS 'Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.';
COMMENT ON COLUMN chat_automation_events.payload IS 'The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.';
COMMENT ON COLUMN chat_automation_events.filter_matched IS 'Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.';
COMMENT ON COLUMN chat_automation_events.resolved_labels IS 'Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.';
COMMENT ON COLUMN chat_automation_events.matched_chat_id IS 'ID of an existing chat that was found via label matching and continued with a new message.';
COMMENT ON COLUMN chat_automation_events.created_chat_id IS 'ID of a newly created chat (mutually exclusive with matched_chat_id in practice).';
COMMENT ON COLUMN chat_automation_events.status IS 'Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.';
CREATE TABLE chat_automation_triggers (
id uuid NOT NULL,
automation_id uuid NOT NULL,
type chat_automation_trigger_type NOT NULL,
webhook_secret text,
webhook_secret_key_id text,
cron_schedule text,
last_triggered_at timestamp with time zone,
filter jsonb,
label_paths jsonb,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT chat_automation_triggers_cron_fields CHECK (((type <> 'cron'::chat_automation_trigger_type) OR ((cron_schedule IS NOT NULL) AND (webhook_secret IS NULL) AND (webhook_secret_key_id IS NULL)))),
CONSTRAINT chat_automation_triggers_webhook_fields CHECK (((type <> 'webhook'::chat_automation_trigger_type) OR ((webhook_secret IS NOT NULL) AND (cron_schedule IS NULL) AND (last_triggered_at IS NULL))))
);
COMMENT ON TABLE chat_automation_triggers IS 'Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.';
COMMENT ON COLUMN chat_automation_triggers.type IS 'Discriminator: webhook or cron. Determines which nullable columns are meaningful.';
COMMENT ON COLUMN chat_automation_triggers.webhook_secret IS 'HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.';
COMMENT ON COLUMN chat_automation_triggers.cron_schedule IS 'Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.';
COMMENT ON COLUMN chat_automation_triggers.last_triggered_at IS 'Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.';
COMMENT ON COLUMN chat_automation_triggers.filter IS 'gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.';
COMMENT ON COLUMN chat_automation_triggers.label_paths IS 'Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.';
CREATE TABLE chat_automations (
id uuid NOT NULL,
owner_id uuid NOT NULL,
organization_id uuid NOT NULL,
name text NOT NULL,
description text DEFAULT ''::text NOT NULL,
instructions text DEFAULT ''::text NOT NULL,
model_config_id uuid,
mcp_server_ids uuid[] DEFAULT '{}'::uuid[] NOT NULL,
allowed_tools text[] DEFAULT '{}'::text[] NOT NULL,
status chat_automation_status DEFAULT 'disabled'::chat_automation_status NOT NULL,
max_chat_creates_per_hour integer DEFAULT 10 NOT NULL,
max_messages_per_hour integer DEFAULT 60 NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT chat_automations_max_chat_creates_per_hour_check CHECK ((max_chat_creates_per_hour > 0)),
CONSTRAINT chat_automations_max_messages_per_hour_check CHECK ((max_messages_per_hour > 0))
);
COMMENT ON TABLE chat_automations IS 'Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.';
COMMENT ON COLUMN chat_automations.owner_id IS 'The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.';
COMMENT ON COLUMN chat_automations.organization_id IS 'Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.';
COMMENT ON COLUMN chat_automations.instructions IS 'The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.';
COMMENT ON COLUMN chat_automations.model_config_id IS 'Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.';
COMMENT ON COLUMN chat_automations.mcp_server_ids IS 'MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.';
COMMENT ON COLUMN chat_automations.allowed_tools IS 'Tool allowlist. Empty means all tools available to the model config are permitted.';
COMMENT ON COLUMN chat_automations.status IS 'Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.';
COMMENT ON COLUMN chat_automations.max_chat_creates_per_hour IS 'Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.';
COMMENT ON COLUMN chat_automations.max_messages_per_hour IS 'Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.';
CREATE TABLE chat_diff_statuses (
chat_id uuid NOT NULL,
url text,
@@ -1404,7 +1533,8 @@ CREATE TABLE chats (
agent_id uuid,
pin_order integer DEFAULT 0 NOT NULL,
last_read_message_id bigint,
last_injected_context jsonb
last_injected_context jsonb,
automation_id uuid
);
CREATE TABLE connection_logs (
@@ -3320,6 +3450,15 @@ ALTER TABLE ONLY audit_logs
ALTER TABLE ONLY boundary_usage_stats
ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
ALTER TABLE ONLY chat_automation_events
ADD CONSTRAINT chat_automation_events_pkey PRIMARY KEY (id);
ALTER TABLE ONLY chat_automation_triggers
ADD CONSTRAINT chat_automation_triggers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY chat_automations
ADD CONSTRAINT chat_automations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY chat_diff_statuses
ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);
@@ -3705,6 +3844,20 @@ CREATE INDEX idx_audit_log_user_id ON audit_logs USING btree (user_id);
CREATE INDEX idx_audit_logs_time_desc ON audit_logs USING btree ("time" DESC);
CREATE INDEX idx_chat_automation_events_automation_id_received_at ON chat_automation_events USING btree (automation_id, received_at DESC);
CREATE INDEX idx_chat_automation_events_rate_limit ON chat_automation_events USING btree (automation_id, received_at) WHERE (status = ANY (ARRAY['created'::chat_automation_event_status, 'continued'::chat_automation_event_status]));
CREATE INDEX idx_chat_automation_events_received_at ON chat_automation_events USING btree (received_at);
CREATE INDEX idx_chat_automation_triggers_automation_id ON chat_automation_triggers USING btree (automation_id);
CREATE INDEX idx_chat_automations_organization_id ON chat_automations USING btree (organization_id);
CREATE INDEX idx_chat_automations_owner_id ON chat_automations USING btree (owner_id);
CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations USING btree (owner_id, organization_id, name);
CREATE INDEX idx_chat_diff_statuses_stale_at ON chat_diff_statuses USING btree (stale_at);
CREATE INDEX idx_chat_files_org ON chat_files USING btree (organization_id);
@@ -3733,6 +3886,8 @@ CREATE INDEX idx_chat_providers_enabled ON chat_providers USING btree (enabled);
CREATE INDEX idx_chat_queued_messages_chat_id ON chat_queued_messages USING btree (chat_id);
CREATE INDEX idx_chats_automation_id ON chats USING btree (automation_id);
CREATE INDEX idx_chats_labels ON chats USING gin (labels);
CREATE INDEX idx_chats_last_model_config_id ON chats USING btree (last_model_config_id);
@@ -4006,6 +4161,33 @@ ALTER TABLE ONLY aibridge_interceptions
ALTER TABLE ONLY api_keys
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_automation_events
ADD CONSTRAINT chat_automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_automation_events
ADD CONSTRAINT chat_automation_events_created_chat_id_fkey FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ALTER TABLE ONLY chat_automation_events
ADD CONSTRAINT chat_automation_events_matched_chat_id_fkey FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ALTER TABLE ONLY chat_automation_events
ADD CONSTRAINT chat_automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL;
ALTER TABLE ONLY chat_automation_triggers
ADD CONSTRAINT chat_automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_automation_triggers
ADD CONSTRAINT chat_automation_triggers_webhook_secret_key_id_fkey FOREIGN KEY (webhook_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ALTER TABLE ONLY chat_automations
ADD CONSTRAINT chat_automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
ALTER TABLE ONLY chat_automations
ADD CONSTRAINT chat_automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_automations
ADD CONSTRAINT chat_automations_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY chat_diff_statuses
ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
@@ -4042,6 +4224,9 @@ ALTER TABLE ONLY chat_queued_messages
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE SET NULL;
ALTER TABLE ONLY chats
ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL;
+10
View File
@@ -9,6 +9,15 @@ const (
ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyChatAutomationEventsAutomationID ForeignKeyConstraint = "chat_automation_events_automation_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
ForeignKeyChatAutomationEventsCreatedChatID ForeignKeyConstraint = "chat_automation_events_created_chat_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_created_chat_id_fkey FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ForeignKeyChatAutomationEventsMatchedChatID ForeignKeyConstraint = "chat_automation_events_matched_chat_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_matched_chat_id_fkey FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
ForeignKeyChatAutomationEventsTriggerID ForeignKeyConstraint = "chat_automation_events_trigger_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL;
ForeignKeyChatAutomationTriggersAutomationID ForeignKeyConstraint = "chat_automation_triggers_automation_id_fkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
ForeignKeyChatAutomationTriggersWebhookSecretKeyID ForeignKeyConstraint = "chat_automation_triggers_webhook_secret_key_id_fkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_webhook_secret_key_id_fkey FOREIGN KEY (webhook_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyChatAutomationsModelConfigID ForeignKeyConstraint = "chat_automations_model_config_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
ForeignKeyChatAutomationsOrganizationID ForeignKeyConstraint = "chat_automations_organization_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyChatAutomationsOwnerID ForeignKeyConstraint = "chat_automations_owner_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -21,6 +30,7 @@ const (
ForeignKeyChatProvidersCreatedBy ForeignKeyConstraint = "chat_providers_created_by_fkey" // ALTER TABLE ONLY chat_providers ADD CONSTRAINT chat_providers_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id);
ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
ForeignKeyChatsAutomationID ForeignKeyConstraint = "chats_automation_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE SET NULL;
ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL;
ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -27,6 +27,7 @@ func TestCustomQueriesSyncedRowScan(t *testing.T) {
"GetWorkspaces": "GetAuthorizedWorkspaces",
"GetUsers": "GetAuthorizedUsers",
"GetChats": "GetAuthorizedChats",
"GetChatAutomations": "GetAuthorizedChatAutomations",
}
// Scan custom
+3
View File
@@ -15,6 +15,9 @@ const (
LockIDReconcilePrebuilds
LockIDReconcileSystemRoles
LockIDBoundaryUsageStats
// LockIDChatAutomationCron prevents concurrent cron trigger
// evaluation across coderd replicas.
LockIDChatAutomationCron
)
// GenLockID generates a unique and consistent lock ID from a given string.
@@ -0,0 +1,13 @@
ALTER TABLE chats DROP COLUMN IF EXISTS automation_id;
DROP TABLE IF EXISTS chat_automation_events;
DROP TABLE IF EXISTS chat_automation_triggers;
DROP TABLE IF EXISTS chat_automations;
DROP TYPE IF EXISTS chat_automation_event_status;
DROP TYPE IF EXISTS chat_automation_trigger_type;
DROP TYPE IF EXISTS chat_automation_status;
@@ -0,0 +1,238 @@
-- Chat automations bridge external events (webhooks, cron schedules) to
-- Coder chats. A chat automation defines *what* to say, *which* model
-- and tools to use, and *how fast* it is allowed to create or continue
-- chats.
CREATE TYPE chat_automation_status AS ENUM ('disabled', 'preview', 'active');
CREATE TYPE chat_automation_trigger_type AS ENUM ('webhook', 'cron');
CREATE TYPE chat_automation_event_status AS ENUM ('filtered', 'preview', 'created', 'continued', 'rate_limited', 'error');
CREATE TABLE chat_automations (
id uuid NOT NULL,
-- The user on whose behalf chats are created. All RBAC checks and
-- chat ownership are scoped to this user.
owner_id uuid NOT NULL,
-- Organization scope for RBAC. Combined with owner_id and name to
-- form a unique constraint so automations are namespaced per user
-- per org.
organization_id uuid NOT NULL,
-- Human-readable identifier. Unique within (owner_id, organization_id).
name text NOT NULL,
-- Optional long-form description shown in the UI.
description text NOT NULL DEFAULT '',
-- The user-role message injected into every chat this automation
-- creates. This is the core prompt that tells the LLM what to do.
instructions text NOT NULL DEFAULT '',
-- Optional model configuration override. When NULL the deployment
-- default is used. SET NULL on delete so automations survive config
-- changes gracefully.
model_config_id uuid,
-- MCP servers to attach to chats created by this automation.
-- Stored as an array of UUIDs rather than a join table because
-- the set is small and always read/written atomically.
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
-- Tool allowlist. Empty means all tools available to the model
-- config are permitted.
allowed_tools text[] NOT NULL DEFAULT '{}',
-- Lifecycle state:
-- disabled — trigger events are silently dropped.
-- preview — events are logged but no chat is created (dry-run).
-- active — events create or continue chats.
status chat_automation_status NOT NULL DEFAULT 'disabled',
-- Maximum number of *new* chats this automation may create in a
-- rolling one-hour window. Prevents runaway webhook storms from
-- flooding the system. Approximate under concurrency; the
-- check-then-insert is not serialized, so brief bursts may
-- slightly exceed the cap.
max_chat_creates_per_hour integer NOT NULL DEFAULT 10,
-- Maximum total messages (creates + continues) this automation may
-- send in a rolling one-hour window. A second, broader throttle
-- that catches high-frequency continuation patterns. Same
-- approximate-under-concurrency caveat as above.
max_messages_per_hour integer NOT NULL DEFAULT 60,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL,
CONSTRAINT chat_automations_max_chat_creates_per_hour_check CHECK (max_chat_creates_per_hour > 0),
CONSTRAINT chat_automations_max_messages_per_hour_check CHECK (max_messages_per_hour > 0)
);
CREATE INDEX idx_chat_automations_owner_id ON chat_automations (owner_id);
CREATE INDEX idx_chat_automations_organization_id ON chat_automations (organization_id);
-- Enforces that automation names are unique per user per org so they
-- can be referenced unambiguously in CLI/API calls.
CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations (owner_id, organization_id, name);
-- Triggers define *how* an automation is invoked. Each automation can
-- have multiple triggers (e.g. one webhook + one cron schedule).
-- Webhook and cron triggers share the same row shape with type-specific
-- nullable columns to keep the schema simple.
CREATE TABLE chat_automation_triggers (
id uuid NOT NULL,
-- Parent automation. CASCADE delete ensures orphan triggers are
-- cleaned up when an automation is removed.
automation_id uuid NOT NULL,
-- Discriminator: 'webhook' or 'cron'. Determines which nullable
-- columns are meaningful.
type chat_automation_trigger_type NOT NULL,
-- HMAC-SHA256 shared secret for webhook signature verification
-- (X-Hub-Signature-256 header). NULL for cron triggers.
webhook_secret text,
-- Identifier of the dbcrypt key used to encrypt webhook_secret.
-- NULL means the secret is not yet encrypted. When dbcrypt is
-- enabled, this references the active key digest used for
-- AES-256-GCM encryption.
webhook_secret_key_id text REFERENCES dbcrypt_keys(active_key_digest),
-- Standard 5-field cron expression (minute hour dom month dow),
-- with optional CRON_TZ= prefix. NULL for webhook triggers.
cron_schedule text,
-- Timestamp of the last successful cron fire. The scheduler
-- computes next = cron.Next(last_triggered_at) and fires when
-- next <= now. NULL means the trigger has never fired; the
-- scheduler falls back to created_at as the reference time.
-- Not used for webhook triggers.
last_triggered_at timestamp with time zone,
-- gjson path→value filter conditions evaluated against the
-- incoming webhook payload. All conditions must match for the
-- trigger to fire. NULL or empty means "match everything".
filter jsonb,
-- Maps chat label keys to gjson paths. When a trigger fires,
-- labels are resolved from the payload and used to find an
-- existing chat to continue (by label match) or set on a
-- newly created chat. This is how automations route events
-- to the right conversation.
label_paths jsonb,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE,
CONSTRAINT chat_automation_triggers_webhook_fields CHECK (
type != 'webhook' OR (webhook_secret IS NOT NULL AND cron_schedule IS NULL AND last_triggered_at IS NULL)
),
CONSTRAINT chat_automation_triggers_cron_fields CHECK (
type != 'cron' OR (cron_schedule IS NOT NULL AND webhook_secret IS NULL AND webhook_secret_key_id IS NULL)
)
);
CREATE INDEX idx_chat_automation_triggers_automation_id ON chat_automation_triggers (automation_id);
-- Every trigger invocation produces an event row regardless of outcome.
-- This table is the audit trail and the data source for rate-limit
-- window counts. Rows are append-only and expected to be purged by a
-- background job after a retention period.
CREATE TABLE chat_automation_events (
id uuid NOT NULL,
-- The automation that owns this event.
automation_id uuid NOT NULL,
-- The trigger that produced this event. SET NULL on delete so
-- historical events survive trigger removal.
trigger_id uuid,
-- When the event was received (webhook delivery time or cron
-- evaluation time). Used for rate-limit window calculations and
-- purge cutoffs.
received_at timestamp with time zone NOT NULL,
-- The raw payload that was evaluated. For webhooks this is the
-- HTTP body; for cron triggers it is a synthetic JSON envelope
-- with schedule metadata.
payload jsonb NOT NULL,
-- Whether the trigger's filter conditions matched. False means
-- the event was dropped before any chat interaction.
filter_matched boolean NOT NULL,
-- Labels resolved from the payload via label_paths. Stored so
-- the event log shows exactly which labels were computed.
resolved_labels jsonb,
-- ID of an existing chat that was found via label matching and
-- continued with a new message.
matched_chat_id uuid,
-- ID of a newly created chat (mutually exclusive with
-- matched_chat_id in practice).
created_chat_id uuid,
-- Outcome of the event:
-- filtered — filter did not match, event dropped.
-- preview — automation is in preview mode, no chat action.
-- created — new chat was created.
-- continued — existing chat was continued.
-- rate_limited — rate limit prevented chat action.
-- error — something went wrong (see error column).
status chat_automation_event_status NOT NULL,
-- Human-readable error description when status = 'error' or
-- 'rate_limited'. NULL for successful outcomes.
error text,
PRIMARY KEY (id),
FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE,
FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL,
FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL,
FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL,
CONSTRAINT chat_automation_events_chat_exclusivity CHECK (
matched_chat_id IS NULL OR created_chat_id IS NULL
)
);
-- Composite index for listing events per automation in reverse
-- chronological order (the primary UI query pattern).
CREATE INDEX idx_chat_automation_events_automation_id_received_at ON chat_automation_events (automation_id, received_at DESC);
-- Standalone index on received_at for the purge job, which deletes
-- events older than the retention period across all automations.
CREATE INDEX idx_chat_automation_events_received_at ON chat_automation_events (received_at);
-- Partial index for rate-limit window count queries, which filter
-- by automation_id and status IN ('created', 'continued').
CREATE INDEX idx_chat_automation_events_rate_limit
ON chat_automation_events (automation_id, received_at)
WHERE status IN ('created', 'continued');
-- Link chats back to the automation that created them. SET NULL on
-- delete so chats survive if the automation is removed. Indexed for
-- lookup queries that list chats spawned by a given automation.
ALTER TABLE chats ADD COLUMN automation_id uuid REFERENCES chat_automations(id) ON DELETE SET NULL;
CREATE INDEX idx_chats_automation_id ON chats (automation_id);
-- Enum type comments.
COMMENT ON TYPE chat_automation_status IS 'Lifecycle state of a chat automation: disabled, preview, or active.';
COMMENT ON TYPE chat_automation_trigger_type IS 'Discriminator for chat automation triggers: webhook or cron.';
COMMENT ON TYPE chat_automation_event_status IS 'Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.';
-- Table comments.
COMMENT ON TABLE chat_automations IS 'Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.';
COMMENT ON TABLE chat_automation_triggers IS 'Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.';
COMMENT ON TABLE chat_automation_events IS 'Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.';
-- Column comments for chat_automations.
COMMENT ON COLUMN chat_automations.owner_id IS 'The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.';
COMMENT ON COLUMN chat_automations.organization_id IS 'Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.';
COMMENT ON COLUMN chat_automations.instructions IS 'The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.';
COMMENT ON COLUMN chat_automations.model_config_id IS 'Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.';
COMMENT ON COLUMN chat_automations.mcp_server_ids IS 'MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.';
COMMENT ON COLUMN chat_automations.allowed_tools IS 'Tool allowlist. Empty means all tools available to the model config are permitted.';
COMMENT ON COLUMN chat_automations.status IS 'Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.';
COMMENT ON COLUMN chat_automations.max_chat_creates_per_hour IS 'Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.';
COMMENT ON COLUMN chat_automations.max_messages_per_hour IS 'Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.';
-- Column comments for chat_automation_triggers.
COMMENT ON COLUMN chat_automation_triggers.type IS 'Discriminator: webhook or cron. Determines which nullable columns are meaningful.';
COMMENT ON COLUMN chat_automation_triggers.webhook_secret IS 'HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.';
COMMENT ON COLUMN chat_automation_triggers.cron_schedule IS 'Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.';
COMMENT ON COLUMN chat_automation_triggers.filter IS 'gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.';
COMMENT ON COLUMN chat_automation_triggers.label_paths IS 'Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.';
COMMENT ON COLUMN chat_automation_triggers.last_triggered_at IS 'Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.';
-- Column comments for chat_automation_events.
COMMENT ON COLUMN chat_automation_events.payload IS 'The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.';
COMMENT ON COLUMN chat_automation_events.filter_matched IS 'Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.';
COMMENT ON COLUMN chat_automation_events.resolved_labels IS 'Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.';
COMMENT ON COLUMN chat_automation_events.matched_chat_id IS 'ID of an existing chat that was found via label matching and continued with a new message.';
COMMENT ON COLUMN chat_automation_events.created_chat_id IS 'ID of a newly created chat (mutually exclusive with matched_chat_id in practice).';
COMMENT ON COLUMN chat_automation_events.status IS 'Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.';
-- Add API key scope values for the new chat_automation resource type.
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:create';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:read';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:update';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:delete';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:*';
@@ -0,0 +1,87 @@
INSERT INTO chat_automations (
id,
owner_id,
organization_id,
name,
description,
instructions,
model_config_id,
mcp_server_ids,
allowed_tools,
status,
max_chat_creates_per_hour,
max_messages_per_hour,
created_at,
updated_at
)
SELECT
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
u.id,
o.id,
'fixture-automation',
'Fixture automation for migration testing.',
'You are a helpful assistant.',
NULL,
'{}',
'{}',
'active',
10,
60,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
FROM users u
CROSS JOIN organizations o
ORDER BY u.created_at, u.id
LIMIT 1;
INSERT INTO chat_automation_triggers (
id,
automation_id,
type,
webhook_secret,
webhook_secret_key_id,
cron_schedule,
last_triggered_at,
filter,
label_paths,
created_at,
updated_at
) VALUES (
'c4e1fe1f-9f2b-4a3d-ab4c-234567890abc',
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
'webhook',
'whsec_fixture_secret',
NULL,
NULL,
NULL,
'{"action": "opened"}'::jsonb,
'{"repo": "repository.full_name"}'::jsonb,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
);
INSERT INTO chat_automation_events (
id,
automation_id,
trigger_id,
received_at,
payload,
filter_matched,
resolved_labels,
matched_chat_id,
created_chat_id,
status,
error
) VALUES (
'd5f20f20-a03c-4b4e-bc5d-345678901bcd',
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
'c4e1fe1f-9f2b-4a3d-ab4c-234567890abc',
'2024-01-01 00:00:00+00',
'{"action": "opened", "repository": {"full_name": "coder/coder"}}'::jsonb,
TRUE,
'{"repo": "coder/coder"}'::jsonb,
NULL,
NULL,
'preview',
NULL
);
+7
View File
@@ -182,6 +182,13 @@ func (r GetChatsRow) RBACObject() rbac.Object {
return r.Chat.RBACObject()
}
func (a ChatAutomation) RBACObject() rbac.Object {
return rbac.ResourceChatAutomation.
WithID(a.ID).
WithOwner(a.OwnerID.String()).
InOrg(a.OrganizationID)
}
func (c ChatFile) RBACObject() rbac.Object {
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
}
+65 -1
View File
@@ -53,6 +53,7 @@ type customQuerier interface {
connectionLogQuerier
aibridgeQuerier
chatQuerier
chatAutomationQuerier
}
type templateQuerier interface {
@@ -796,7 +797,70 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
&i.Chat.PinOrder,
&i.Chat.LastReadMessageID,
&i.Chat.LastInjectedContext,
&i.HasUnread); err != nil {
&i.Chat.AutomationID,
&i.HasUnread,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
type chatAutomationQuerier interface {
GetAuthorizedChatAutomations(ctx context.Context, arg GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]ChatAutomation, error)
}
func (q *sqlQuerier) GetAuthorizedChatAutomations(ctx context.Context, arg GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]ChatAutomation, error) {
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
VariableConverter: regosql.NoACLConverter(),
})
if err != nil {
return nil, xerrors.Errorf("compile authorized filter: %w", err)
}
filtered, err := insertAuthorizedFilter(getChatAutomations, fmt.Sprintf(" AND %s", authorizedFilter))
if err != nil {
return nil, xerrors.Errorf("insert authorized filter: %w", err)
}
// The name comment is for metric tracking
query := fmt.Sprintf("-- name: GetAuthorizedChatAutomations :many\n%s", filtered)
rows, err := q.db.QueryContext(ctx, query,
arg.OwnerID,
arg.OrganizationID,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatAutomation
for rows.Next() {
var i ChatAutomation
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.Name,
&i.Description,
&i.Instructions,
&i.ModelConfigID,
pq.Array(&i.MCPServerIDs),
pq.Array(&i.AllowedTools),
&i.Status,
&i.MaxChatCreatesPerHour,
&i.MaxMessagesPerHour,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
+278 -1
View File
@@ -224,6 +224,11 @@ const (
ApiKeyScopeChatUpdate APIKeyScope = "chat:update"
ApiKeyScopeChatDelete APIKeyScope = "chat:delete"
ApiKeyScopeChat APIKeyScope = "chat:*"
ApiKeyScopeChatAutomationCreate APIKeyScope = "chat_automation:create"
ApiKeyScopeChatAutomationRead APIKeyScope = "chat_automation:read"
ApiKeyScopeChatAutomationUpdate APIKeyScope = "chat_automation:update"
ApiKeyScopeChatAutomationDelete APIKeyScope = "chat_automation:delete"
ApiKeyScopeChatAutomation APIKeyScope = "chat_automation:*"
)
func (e *APIKeyScope) Scan(src interface{}) error {
@@ -467,7 +472,12 @@ func (e APIKeyScope) Valid() bool {
ApiKeyScopeChatRead,
ApiKeyScopeChatUpdate,
ApiKeyScopeChatDelete,
ApiKeyScopeChat:
ApiKeyScopeChat,
ApiKeyScopeChatAutomationCreate,
ApiKeyScopeChatAutomationRead,
ApiKeyScopeChatAutomationUpdate,
ApiKeyScopeChatAutomationDelete,
ApiKeyScopeChatAutomation:
return true
}
return false
@@ -680,6 +690,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
ApiKeyScopeChatUpdate,
ApiKeyScopeChatDelete,
ApiKeyScopeChat,
ApiKeyScopeChatAutomationCreate,
ApiKeyScopeChatAutomationRead,
ApiKeyScopeChatAutomationUpdate,
ApiKeyScopeChatAutomationDelete,
ApiKeyScopeChatAutomation,
}
}
@@ -1107,6 +1122,198 @@ func AllBuildReasonValues() []BuildReason {
}
}
// Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.
type ChatAutomationEventStatus string
const (
ChatAutomationEventStatusFiltered ChatAutomationEventStatus = "filtered"
ChatAutomationEventStatusPreview ChatAutomationEventStatus = "preview"
ChatAutomationEventStatusCreated ChatAutomationEventStatus = "created"
ChatAutomationEventStatusContinued ChatAutomationEventStatus = "continued"
ChatAutomationEventStatusRateLimited ChatAutomationEventStatus = "rate_limited"
ChatAutomationEventStatusError ChatAutomationEventStatus = "error"
)
func (e *ChatAutomationEventStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ChatAutomationEventStatus(s)
case string:
*e = ChatAutomationEventStatus(s)
default:
return fmt.Errorf("unsupported scan type for ChatAutomationEventStatus: %T", src)
}
return nil
}
type NullChatAutomationEventStatus struct {
ChatAutomationEventStatus ChatAutomationEventStatus `json:"chat_automation_event_status"`
Valid bool `json:"valid"` // Valid is true if ChatAutomationEventStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullChatAutomationEventStatus) Scan(value interface{}) error {
if value == nil {
ns.ChatAutomationEventStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ChatAutomationEventStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullChatAutomationEventStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ChatAutomationEventStatus), nil
}
func (e ChatAutomationEventStatus) Valid() bool {
switch e {
case ChatAutomationEventStatusFiltered,
ChatAutomationEventStatusPreview,
ChatAutomationEventStatusCreated,
ChatAutomationEventStatusContinued,
ChatAutomationEventStatusRateLimited,
ChatAutomationEventStatusError:
return true
}
return false
}
func AllChatAutomationEventStatusValues() []ChatAutomationEventStatus {
return []ChatAutomationEventStatus{
ChatAutomationEventStatusFiltered,
ChatAutomationEventStatusPreview,
ChatAutomationEventStatusCreated,
ChatAutomationEventStatusContinued,
ChatAutomationEventStatusRateLimited,
ChatAutomationEventStatusError,
}
}
// Lifecycle state of a chat automation: disabled, preview, or active.
type ChatAutomationStatus string
const (
ChatAutomationStatusDisabled ChatAutomationStatus = "disabled"
ChatAutomationStatusPreview ChatAutomationStatus = "preview"
ChatAutomationStatusActive ChatAutomationStatus = "active"
)
func (e *ChatAutomationStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ChatAutomationStatus(s)
case string:
*e = ChatAutomationStatus(s)
default:
return fmt.Errorf("unsupported scan type for ChatAutomationStatus: %T", src)
}
return nil
}
type NullChatAutomationStatus struct {
ChatAutomationStatus ChatAutomationStatus `json:"chat_automation_status"`
Valid bool `json:"valid"` // Valid is true if ChatAutomationStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullChatAutomationStatus) Scan(value interface{}) error {
if value == nil {
ns.ChatAutomationStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ChatAutomationStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullChatAutomationStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ChatAutomationStatus), nil
}
func (e ChatAutomationStatus) Valid() bool {
switch e {
case ChatAutomationStatusDisabled,
ChatAutomationStatusPreview,
ChatAutomationStatusActive:
return true
}
return false
}
func AllChatAutomationStatusValues() []ChatAutomationStatus {
return []ChatAutomationStatus{
ChatAutomationStatusDisabled,
ChatAutomationStatusPreview,
ChatAutomationStatusActive,
}
}
// Discriminator for chat automation triggers: webhook or cron.
type ChatAutomationTriggerType string
const (
ChatAutomationTriggerTypeWebhook ChatAutomationTriggerType = "webhook"
ChatAutomationTriggerTypeCron ChatAutomationTriggerType = "cron"
)
func (e *ChatAutomationTriggerType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ChatAutomationTriggerType(s)
case string:
*e = ChatAutomationTriggerType(s)
default:
return fmt.Errorf("unsupported scan type for ChatAutomationTriggerType: %T", src)
}
return nil
}
type NullChatAutomationTriggerType struct {
ChatAutomationTriggerType ChatAutomationTriggerType `json:"chat_automation_trigger_type"`
Valid bool `json:"valid"` // Valid is true if ChatAutomationTriggerType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullChatAutomationTriggerType) Scan(value interface{}) error {
if value == nil {
ns.ChatAutomationTriggerType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ChatAutomationTriggerType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullChatAutomationTriggerType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ChatAutomationTriggerType), nil
}
func (e ChatAutomationTriggerType) Valid() bool {
switch e {
case ChatAutomationTriggerTypeWebhook,
ChatAutomationTriggerTypeCron:
return true
}
return false
}
func AllChatAutomationTriggerTypeValues() []ChatAutomationTriggerType {
return []ChatAutomationTriggerType{
ChatAutomationTriggerTypeWebhook,
ChatAutomationTriggerTypeCron,
}
}
type ChatMessageRole string
const (
@@ -4176,6 +4383,76 @@ type Chat struct {
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
AutomationID uuid.NullUUID `db:"automation_id" json:"automation_id"`
}
// Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.
type ChatAutomation struct {
ID uuid.UUID `db:"id" json:"id"`
// The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
// Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
// The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.
Instructions string `db:"instructions" json:"instructions"`
// Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
// MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
// Tool allowlist. Empty means all tools available to the model config are permitted.
AllowedTools []string `db:"allowed_tools" json:"allowed_tools"`
// Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.
Status ChatAutomationStatus `db:"status" json:"status"`
// Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.
MaxChatCreatesPerHour int32 `db:"max_chat_creates_per_hour" json:"max_chat_creates_per_hour"`
// Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.
MaxMessagesPerHour int32 `db:"max_messages_per_hour" json:"max_messages_per_hour"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.
type ChatAutomationEvent struct {
ID uuid.UUID `db:"id" json:"id"`
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
TriggerID uuid.NullUUID `db:"trigger_id" json:"trigger_id"`
ReceivedAt time.Time `db:"received_at" json:"received_at"`
// The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.
Payload json.RawMessage `db:"payload" json:"payload"`
// Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.
FilterMatched bool `db:"filter_matched" json:"filter_matched"`
// Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.
ResolvedLabels pqtype.NullRawMessage `db:"resolved_labels" json:"resolved_labels"`
// ID of an existing chat that was found via label matching and continued with a new message.
MatchedChatID uuid.NullUUID `db:"matched_chat_id" json:"matched_chat_id"`
// ID of a newly created chat (mutually exclusive with matched_chat_id in practice).
CreatedChatID uuid.NullUUID `db:"created_chat_id" json:"created_chat_id"`
// Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.
Status ChatAutomationEventStatus `db:"status" json:"status"`
Error sql.NullString `db:"error" json:"error"`
}
// Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.
type ChatAutomationTrigger struct {
ID uuid.UUID `db:"id" json:"id"`
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
// Discriminator: webhook or cron. Determines which nullable columns are meaningful.
Type ChatAutomationTriggerType `db:"type" json:"type"`
// HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.
WebhookSecret sql.NullString `db:"webhook_secret" json:"webhook_secret"`
WebhookSecretKeyID sql.NullString `db:"webhook_secret_key_id" json:"webhook_secret_key_id"`
// Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
// Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.
LastTriggeredAt sql.NullTime `db:"last_triggered_at" json:"last_triggered_at"`
// gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
// Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.
LabelPaths pqtype.NullRawMessage `db:"label_paths" json:"label_paths"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
type ChatDiffStatus struct {
+33
View File
@@ -74,10 +74,21 @@ type sqlcQuerier interface {
CleanTailnetCoordinators(ctx context.Context) error
CleanTailnetLostPeers(ctx context.Context) error
CleanTailnetTunnels(ctx context.Context) error
CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error
CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error
CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error)
CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error)
CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error)
// Counts new-chat events in the rate-limit window. This count is
// approximate under concurrency: concurrent webhook handlers may
// each read the same count before any of them insert, so brief
// bursts can slightly exceed the configured cap.
CountChatAutomationChatCreatesInWindow(ctx context.Context, arg CountChatAutomationChatCreatesInWindowParams) (int64, error)
// Counts total message events (creates + continues) in the rate-limit
// window. This count is approximate under concurrency: concurrent
// webhook handlers may each read the same count before any of them
// insert, so brief bursts can slightly exceed the configured cap.
CountChatAutomationMessagesInWindow(ctx context.Context, arg CountChatAutomationMessagesInWindowParams) (int64, error)
CountConnectionLogs(ctx context.Context, arg CountConnectionLogsParams) (int64, error)
// Counts enabled, non-deleted model configs that lack both input and
// output pricing in their JSONB options.cost configuration.
@@ -100,6 +111,8 @@ type sqlcQuerier interface {
// be recreated.
DeleteAllWebpushSubscriptions(ctx context.Context) error
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error
DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error
DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error
DeleteChatProviderByID(ctx context.Context, id uuid.UUID) error
DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error
@@ -197,6 +210,10 @@ type sqlcQuerier interface {
GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error)
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
GetActiveAISeatCount(ctx context.Context) (int64, error)
// Returns all cron triggers whose parent automation is active or in
// preview mode. The scheduler uses this to evaluate which triggers
// are due.
GetActiveChatAutomationCronTriggers(ctx context.Context) ([]GetActiveChatAutomationCronTriggersRow, error)
GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error)
GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error)
GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error)
@@ -223,6 +240,11 @@ type sqlcQuerier interface {
// This function returns roles for authorization purposes. Implied member roles
// are included.
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
GetChatAutomationByID(ctx context.Context, id uuid.UUID) (ChatAutomation, error)
GetChatAutomationEventsByAutomationID(ctx context.Context, arg GetChatAutomationEventsByAutomationIDParams) ([]ChatAutomationEvent, error)
GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (ChatAutomationTrigger, error)
GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]ChatAutomationTrigger, error)
GetChatAutomations(ctx context.Context, arg GetChatAutomationsParams) ([]ChatAutomation, error)
GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error)
GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error)
// Per-root-chat cost breakdown for a single user within a date range.
@@ -696,6 +718,9 @@ type sqlcQuerier interface {
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error)
InsertChatAutomation(ctx context.Context, arg InsertChatAutomationParams) (ChatAutomation, error)
InsertChatAutomationEvent(ctx context.Context, arg InsertChatAutomationEventParams) (ChatAutomationEvent, error)
InsertChatAutomationTrigger(ctx context.Context, arg InsertChatAutomationTriggerParams) (ChatAutomationTrigger, error)
InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error)
InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error)
InsertChatModelConfig(ctx context.Context, arg InsertChatModelConfigParams) (ChatModelConfig, error)
@@ -822,6 +847,10 @@ type sqlcQuerier interface {
// sequence, so this is acceptable.
PinChatByID(ctx context.Context, id uuid.UUID) error
PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error)
// Deletes old chat automation events in bounded batches to avoid
// long-running locks on high-volume tables. Callers should loop
// until zero rows are returned.
PurgeOldChatAutomationEvents(ctx context.Context, arg PurgeOldChatAutomationEventsParams) (int64, error)
ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error)
@@ -852,6 +881,10 @@ type sqlcQuerier interface {
UnsetDefaultChatModelConfigs(ctx context.Context) error
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateChatAutomation(ctx context.Context, arg UpdateChatAutomationParams) (ChatAutomation, error)
UpdateChatAutomationTrigger(ctx context.Context, arg UpdateChatAutomationTriggerParams) (ChatAutomationTrigger, error)
UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg UpdateChatAutomationTriggerLastTriggeredAtParams) error
UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg UpdateChatAutomationTriggerWebhookSecretParams) (ChatAutomationTrigger, error)
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
// Bumps the heartbeat timestamp for a running chat so that other
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
-- name: InsertChatAutomationEvent :one
INSERT INTO chat_automation_events (
id,
automation_id,
trigger_id,
received_at,
payload,
filter_matched,
resolved_labels,
matched_chat_id,
created_chat_id,
status,
error
) VALUES (
@id::uuid,
@automation_id::uuid,
sqlc.narg('trigger_id')::uuid,
@received_at::timestamptz,
@payload::jsonb,
@filter_matched::boolean,
sqlc.narg('resolved_labels')::jsonb,
sqlc.narg('matched_chat_id')::uuid,
sqlc.narg('created_chat_id')::uuid,
@status::chat_automation_event_status,
sqlc.narg('error')::text
) RETURNING *;
-- name: GetChatAutomationEventsByAutomationID :many
SELECT
*
FROM
chat_automation_events
WHERE
automation_id = @automation_id::uuid
AND CASE
WHEN sqlc.narg('status_filter')::chat_automation_event_status IS NOT NULL THEN status = sqlc.narg('status_filter')::chat_automation_event_status
ELSE true
END
ORDER BY
received_at DESC
OFFSET @offset_opt
LIMIT
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
-- name: CountChatAutomationChatCreatesInWindow :one
-- Counts new-chat events in the rate-limit window. This count is
-- approximate under concurrency: concurrent webhook handlers may
-- each read the same count before any of them insert, so brief
-- bursts can slightly exceed the configured cap.
SELECT COUNT(*)
FROM chat_automation_events
WHERE automation_id = @automation_id::uuid
AND status = 'created'
AND received_at > @window_start::timestamptz;
-- name: CountChatAutomationMessagesInWindow :one
-- Counts total message events (creates + continues) in the rate-limit
-- window. This count is approximate under concurrency: concurrent
-- webhook handlers may each read the same count before any of them
-- insert, so brief bursts can slightly exceed the configured cap.
SELECT COUNT(*)
FROM chat_automation_events
WHERE automation_id = @automation_id::uuid
AND status IN ('created', 'continued')
AND received_at > @window_start::timestamptz;
-- name: PurgeOldChatAutomationEvents :execrows
-- Deletes old chat automation events in bounded batches to avoid
-- long-running locks on high-volume tables. Callers should loop
-- until zero rows are returned.
WITH old_events AS (
SELECT id
FROM chat_automation_events
WHERE received_at < @before::timestamptz
ORDER BY received_at ASC
LIMIT @limit_count
)
DELETE FROM chat_automation_events
USING old_events
WHERE chat_automation_events.id = old_events.id;
@@ -0,0 +1,85 @@
-- name: InsertChatAutomation :one
INSERT INTO chat_automations (
id,
owner_id,
organization_id,
name,
description,
instructions,
model_config_id,
mcp_server_ids,
allowed_tools,
status,
max_chat_creates_per_hour,
max_messages_per_hour,
created_at,
updated_at
) VALUES (
@id::uuid,
@owner_id::uuid,
@organization_id::uuid,
@name::text,
@description::text,
@instructions::text,
sqlc.narg('model_config_id')::uuid,
COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]),
COALESCE(@allowed_tools::text[], '{}'::text[]),
@status::chat_automation_status,
@max_chat_creates_per_hour::integer,
@max_messages_per_hour::integer,
@created_at::timestamptz,
@updated_at::timestamptz
) RETURNING *;
-- name: GetChatAutomationByID :one
SELECT * FROM chat_automations WHERE id = @id::uuid;
-- name: GetChatAutomations :many
SELECT
*
FROM
chat_automations
WHERE
CASE
WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chat_automations.owner_id = @owner_id
ELSE true
END
AND CASE
WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chat_automations.organization_id = @organization_id
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedChatAutomations
-- @authorize_filter
ORDER BY
created_at DESC, id DESC
OFFSET @offset_opt
LIMIT
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
-- name: UpdateChatAutomation :one
UPDATE chat_automations SET
name = @name::text,
description = @description::text,
instructions = @instructions::text,
model_config_id = sqlc.narg('model_config_id')::uuid,
mcp_server_ids = COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]),
allowed_tools = COALESCE(@allowed_tools::text[], '{}'::text[]),
status = @status::chat_automation_status,
max_chat_creates_per_hour = @max_chat_creates_per_hour::integer,
max_messages_per_hour = @max_messages_per_hour::integer,
updated_at = @updated_at::timestamptz
WHERE id = @id::uuid
RETURNING *;
-- name: DeleteChatAutomationByID :exec
DELETE FROM chat_automations WHERE id = @id::uuid;
-- name: CleanupDeletedMCPServerIDsFromChatAutomations :exec
UPDATE chat_automations
SET mcp_server_ids = (
SELECT COALESCE(array_agg(sid), '{}')
FROM unnest(chat_automations.mcp_server_ids) AS sid
WHERE sid IN (SELECT id FROM mcp_server_configs)
)
WHERE mcp_server_ids != '{}'
AND NOT (mcp_server_ids <@ COALESCE((SELECT array_agg(id) FROM mcp_server_configs), '{}'));
@@ -0,0 +1,87 @@
-- name: InsertChatAutomationTrigger :one
INSERT INTO chat_automation_triggers (
id,
automation_id,
type,
webhook_secret,
webhook_secret_key_id,
cron_schedule,
filter,
label_paths,
created_at,
updated_at
) VALUES (
@id::uuid,
@automation_id::uuid,
@type::chat_automation_trigger_type,
sqlc.narg('webhook_secret')::text,
sqlc.narg('webhook_secret_key_id')::text,
sqlc.narg('cron_schedule')::text,
sqlc.narg('filter')::jsonb,
sqlc.narg('label_paths')::jsonb,
@created_at::timestamptz,
@updated_at::timestamptz
) RETURNING *;
-- name: GetChatAutomationTriggerByID :one
SELECT * FROM chat_automation_triggers WHERE id = @id::uuid;
-- name: GetChatAutomationTriggersByAutomationID :many
SELECT * FROM chat_automation_triggers
WHERE automation_id = @automation_id::uuid
ORDER BY created_at ASC;
-- name: UpdateChatAutomationTrigger :one
UPDATE chat_automation_triggers SET
cron_schedule = COALESCE(sqlc.narg('cron_schedule'), cron_schedule),
filter = COALESCE(sqlc.narg('filter'), filter),
label_paths = COALESCE(sqlc.narg('label_paths'), label_paths),
updated_at = @updated_at::timestamptz
WHERE id = @id::uuid
RETURNING *;
-- name: UpdateChatAutomationTriggerWebhookSecret :one
UPDATE chat_automation_triggers SET
webhook_secret = sqlc.narg('webhook_secret')::text,
webhook_secret_key_id = sqlc.narg('webhook_secret_key_id')::text,
updated_at = @updated_at::timestamptz
WHERE id = @id::uuid
RETURNING *;
-- name: DeleteChatAutomationTriggerByID :exec
DELETE FROM chat_automation_triggers WHERE id = @id::uuid;
-- name: GetActiveChatAutomationCronTriggers :many
-- Returns all cron triggers whose parent automation is active or in
-- preview mode. The scheduler uses this to evaluate which triggers
-- are due.
SELECT
t.id,
t.automation_id,
t.type,
t.cron_schedule,
t.filter,
t.label_paths,
t.last_triggered_at,
t.created_at,
t.updated_at,
a.status AS automation_status,
a.owner_id AS automation_owner_id,
a.instructions AS automation_instructions,
a.name AS automation_name,
a.organization_id AS automation_organization_id,
a.model_config_id AS automation_model_config_id,
a.mcp_server_ids AS automation_mcp_server_ids,
a.allowed_tools AS automation_allowed_tools,
a.max_chat_creates_per_hour AS automation_max_chat_creates_per_hour,
a.max_messages_per_hour AS automation_max_messages_per_hour
FROM chat_automation_triggers t
JOIN chat_automations a ON a.id = t.automation_id
WHERE t.type = 'cron'
AND t.cron_schedule IS NOT NULL
AND a.status IN ('active', 'preview');
-- name: UpdateChatAutomationTriggerLastTriggeredAt :exec
UPDATE chat_automation_triggers
SET last_triggered_at = @last_triggered_at::timestamptz
WHERE id = @id::uuid;
+2
View File
@@ -247,6 +247,8 @@ sql:
mcp_server_tool_snapshots: MCPServerToolSnapshots
mcp_server_config_id: MCPServerConfigID
mcp_server_ids: MCPServerIDs
automation_mcp_server_ids: AutomationMCPServerIDs
webhook_secret_key_id: WebhookSecretKeyID
icon_url: IconURL
oauth2_client_id: OAuth2ClientID
oauth2_client_secret: OAuth2ClientSecret
+4
View File
@@ -15,6 +15,9 @@ const (
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
UniqueBoundaryUsageStatsPkey UniqueConstraint = "boundary_usage_stats_pkey" // ALTER TABLE ONLY boundary_usage_stats ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
UniqueChatAutomationEventsPkey UniqueConstraint = "chat_automation_events_pkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_pkey PRIMARY KEY (id);
UniqueChatAutomationTriggersPkey UniqueConstraint = "chat_automation_triggers_pkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_pkey PRIMARY KEY (id);
UniqueChatAutomationsPkey UniqueConstraint = "chat_automations_pkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_pkey PRIMARY KEY (id);
UniqueChatDiffStatusesPkey UniqueConstraint = "chat_diff_statuses_pkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);
UniqueChatFilesPkey UniqueConstraint = "chat_files_pkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_pkey PRIMARY KEY (id);
UniqueChatMessagesPkey UniqueConstraint = "chat_messages_pkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id);
@@ -125,6 +128,7 @@ const (
UniqueWorkspaceResourcesPkey UniqueConstraint = "workspace_resources_pkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
UniqueIndexChatAutomationsOwnerOrgName UniqueConstraint = "idx_chat_automations_owner_org_name" // CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations USING btree (owner_id, organization_id, name);
UniqueIndexChatModelConfigsSingleDefault UniqueConstraint = "idx_chat_model_configs_single_default" // CREATE UNIQUE INDEX idx_chat_model_configs_single_default ON chat_model_configs USING btree ((1)) WHERE ((is_default = true) AND (deleted = false));
UniqueIndexConnectionLogsConnectionIDWorkspaceIDAgentName UniqueConstraint = "idx_connection_logs_connection_id_workspace_id_agent_name" // CREATE UNIQUE INDEX idx_connection_logs_connection_id_workspace_id_agent_name ON connection_logs USING btree (connection_id, workspace_id, agent_name);
UniqueIndexCustomRolesNameLowerOrganizationID UniqueConstraint = "idx_custom_roles_name_lower_organization_id" // CREATE UNIQUE INDEX idx_custom_roles_name_lower_organization_id ON custom_roles USING btree (lower(name), COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid));
+11
View File
@@ -82,6 +82,16 @@ var (
Type: "chat",
}
// ResourceChatAutomation
// Valid Actions
// - "ActionCreate" :: create a chat automation
// - "ActionDelete" :: delete a chat automation
// - "ActionRead" :: read chat automation configuration
// - "ActionUpdate" :: update a chat automation
ResourceChatAutomation = Object{
Type: "chat_automation",
}
// ResourceConnectionLog
// Valid Actions
// - "ActionRead" :: read connection logs
@@ -440,6 +450,7 @@ func AllResources() []Objecter {
ResourceAuditLog,
ResourceBoundaryUsage,
ResourceChat,
ResourceChatAutomation,
ResourceConnectionLog,
ResourceCryptoKey,
ResourceDebugInfo,
+10
View File
@@ -84,6 +84,13 @@ var chatActions = map[Action]ActionDefinition{
ActionDelete: "delete a chat",
}
var chatAutomationActions = map[Action]ActionDefinition{
ActionCreate: "create a chat automation",
ActionRead: "read chat automation configuration",
ActionUpdate: "update a chat automation",
ActionDelete: "delete a chat automation",
}
// RBACPermissions is indexed by the type
var RBACPermissions = map[string]PermissionDefinition{
// Wildcard is every object, and the action "*" provides all actions.
@@ -113,6 +120,9 @@ var RBACPermissions = map[string]PermissionDefinition{
"chat": {
Actions: chatActions,
},
"chat_automation": {
Actions: chatAutomationActions,
},
// Dormant workspaces have the same perms as workspaces.
"workspace_dormant": {
Actions: workspaceActions,
+13
View File
@@ -1082,6 +1082,19 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
// Chat automations are admin-managed. Regular org
// members cannot manage automations even if they
// are the owner. The owner_id field is for audit
// tracking, not RBAC grants.
Name: "ChatAutomation",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceChatAutomation.InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin},
false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin},
},
},
}
// Build coverage set from test case definitions statically,
// so we don't need shared mutable state during execution.
+12
View File
@@ -32,6 +32,10 @@ const (
ScopeChatDelete ScopeName = "chat:delete"
ScopeChatRead ScopeName = "chat:read"
ScopeChatUpdate ScopeName = "chat:update"
ScopeChatAutomationCreate ScopeName = "chat_automation:create"
ScopeChatAutomationDelete ScopeName = "chat_automation:delete"
ScopeChatAutomationRead ScopeName = "chat_automation:read"
ScopeChatAutomationUpdate ScopeName = "chat_automation:update"
ScopeConnectionLogRead ScopeName = "connection_log:read"
ScopeConnectionLogUpdate ScopeName = "connection_log:update"
ScopeCryptoKeyCreate ScopeName = "crypto_key:create"
@@ -196,6 +200,10 @@ func (e ScopeName) Valid() bool {
ScopeChatDelete,
ScopeChatRead,
ScopeChatUpdate,
ScopeChatAutomationCreate,
ScopeChatAutomationDelete,
ScopeChatAutomationRead,
ScopeChatAutomationUpdate,
ScopeConnectionLogRead,
ScopeConnectionLogUpdate,
ScopeCryptoKeyCreate,
@@ -361,6 +369,10 @@ func AllScopeNameValues() []ScopeName {
ScopeChatDelete,
ScopeChatRead,
ScopeChatUpdate,
ScopeChatAutomationCreate,
ScopeChatAutomationDelete,
ScopeChatAutomationRead,
ScopeChatAutomationUpdate,
ScopeConnectionLogRead,
ScopeConnectionLogUpdate,
ScopeCryptoKeyCreate,
+5
View File
@@ -38,6 +38,11 @@ const (
APIKeyScopeChatDelete APIKeyScope = "chat:delete"
APIKeyScopeChatRead APIKeyScope = "chat:read"
APIKeyScopeChatUpdate APIKeyScope = "chat:update"
APIKeyScopeChatAutomationAll APIKeyScope = "chat_automation:*"
APIKeyScopeChatAutomationCreate APIKeyScope = "chat_automation:create"
APIKeyScopeChatAutomationDelete APIKeyScope = "chat_automation:delete"
APIKeyScopeChatAutomationRead APIKeyScope = "chat_automation:read"
APIKeyScopeChatAutomationUpdate APIKeyScope = "chat_automation:update"
APIKeyScopeCoderAll APIKeyScope = "coder:all"
APIKeyScopeCoderApikeysManageSelf APIKeyScope = "coder:apikeys.manage_self"
APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect"
+2
View File
@@ -12,6 +12,7 @@ const (
ResourceAuditLog RBACResource = "audit_log"
ResourceBoundaryUsage RBACResource = "boundary_usage"
ResourceChat RBACResource = "chat"
ResourceChatAutomation RBACResource = "chat_automation"
ResourceConnectionLog RBACResource = "connection_log"
ResourceCryptoKey RBACResource = "crypto_key"
ResourceDebugInfo RBACResource = "debug_info"
@@ -84,6 +85,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceAuditLog: {ActionCreate, ActionRead},
ResourceBoundaryUsage: {ActionDelete, ActionRead, ActionUpdate},
ResourceChat: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceChatAutomation: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceConnectionLog: {ActionRead, ActionUpdate},
ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceDebugInfo: {ActionRead},
+20 -20
View File
@@ -193,10 +193,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Property | Value(s) |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -326,10 +326,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Property | Value(s) |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -459,10 +459,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Property | Value(s) |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -554,10 +554,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Property | Value(s) |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -960,9 +960,9 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Property | Value(s) |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+6 -6
View File
@@ -1286,9 +1286,9 @@
#### Enumerated Values
| Value(s) |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` |
| Value(s) |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:update`, `chat_automation:*`, `chat_automation:create`, `chat_automation:delete`, `chat_automation:read`, `chat_automation:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` |
## codersdk.AddLicenseRequest
@@ -8080,9 +8080,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
#### Enumerated Values
| Value(s) |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| Value(s) |
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
## codersdk.RateLimitConfig
+5 -5
View File
@@ -849,11 +849,11 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| `login_type` | `github`, `oidc`, `password`, `token` |
| `scope` | `all`, `application_connect` |
| Property | Value(s) |
|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `chat_automation`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
| `login_type` | `github`, `oidc`, `password`, `token` |
| `scope` | `all`, `application_connect` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+73 -2
View File
@@ -5,10 +5,12 @@ import (
"database/sql"
"strings"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
)
// Rotate rotates the database encryption keys by re-encrypting all user tokens
@@ -109,6 +111,30 @@ func Rotate(ctx context.Context, log slog.Logger, sqlDB *sql.DB, ciphers []Ciphe
log.Debug(ctx, "encrypted chat provider key", slog.F("provider", provider.Provider), slog.F("current", idx+1), slog.F("cipher", ciphers[0].HexDigest()))
}
// Re-encrypt chat automation webhook secrets.
triggerIDs, err := fetchEncryptedTriggerIDs(ctx, sqlDB)
if err != nil {
return err
}
log.Info(ctx, "encrypting chat automation webhook secrets", slog.F("trigger_count", len(triggerIDs)))
for _, triggerID := range triggerIDs {
trigger, err := cryptDB.GetChatAutomationTriggerByID(ctx, triggerID)
if err != nil {
return xerrors.Errorf("get chat automation trigger %s: %w", triggerID, err)
}
if trigger.WebhookSecretKeyID.String == ciphers[0].HexDigest() {
continue // Already encrypted with the primary key.
}
if _, err := cryptDB.UpdateChatAutomationTriggerWebhookSecret(ctx, database.UpdateChatAutomationTriggerWebhookSecretParams{
WebhookSecret: trigger.WebhookSecret, // decrypted by cryptDB
WebhookSecretKeyID: sql.NullString{}, // dbcrypt will set the new primary key
UpdatedAt: dbtime.Now(),
ID: triggerID,
}); err != nil {
return xerrors.Errorf("re-encrypt chat automation trigger %s: %w", triggerID, err)
}
}
// Revoke old keys
for _, c := range ciphers[1:] {
if err := db.RevokeDBCryptKey(ctx, c.HexDigest()); err != nil {
@@ -221,6 +247,27 @@ func Decrypt(ctx context.Context, log slog.Logger, sqlDB *sql.DB, ciphers []Ciph
log.Debug(ctx, "decrypted chat provider key", slog.F("provider", provider.Provider), slog.F("current", idx+1), slog.F("cipher", ciphers[0].HexDigest()))
}
// Decrypt chat automation webhook secrets.
triggerIDs, err := fetchEncryptedTriggerIDs(ctx, sqlDB)
if err != nil {
return err
}
log.Info(ctx, "decrypting chat automation webhook secrets", slog.F("trigger_count", len(triggerIDs)))
for _, triggerID := range triggerIDs {
trigger, err := cryptDB.GetChatAutomationTriggerByID(ctx, triggerID)
if err != nil {
return xerrors.Errorf("get chat automation trigger %s: %w", triggerID, err)
}
if _, err := cryptDB.UpdateChatAutomationTriggerWebhookSecret(ctx, database.UpdateChatAutomationTriggerWebhookSecretParams{
WebhookSecret: trigger.WebhookSecret, // decrypted by cryptDB
WebhookSecretKeyID: sql.NullString{}, // store in plaintext
UpdatedAt: dbtime.Now(),
ID: triggerID,
}); err != nil {
return xerrors.Errorf("decrypt chat automation trigger %s: %w", triggerID, err)
}
}
// Revoke _all_ keys
for _, c := range ciphers {
if err := db.RevokeDBCryptKey(ctx, c.HexDigest()); err != nil {
@@ -245,9 +292,33 @@ UPDATE chat_providers
SET api_key = '',
api_key_key_id = NULL
WHERE api_key_key_id IS NOT NULL;
DELETE FROM chat_automation_triggers WHERE webhook_secret_key_id IS NOT NULL;
COMMIT;
`
// fetchEncryptedTriggerIDs returns the IDs of all chat automation
// triggers that have an encrypted webhook secret. It uses a raw
// SQL query because there is no "get all triggers" SQLC query.
func fetchEncryptedTriggerIDs(ctx context.Context, sqlDB *sql.DB) ([]uuid.UUID, error) {
rows, err := sqlDB.QueryContext(ctx, `SELECT id FROM chat_automation_triggers WHERE webhook_secret_key_id IS NOT NULL`)
if err != nil {
return nil, xerrors.Errorf("get encrypted chat automation triggers: %w", err)
}
defer rows.Close()
var ids []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, xerrors.Errorf("scan chat automation trigger id: %w", err)
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, xerrors.Errorf("iterate chat automation trigger ids: %w", err)
}
return ids, nil
}
// Delete deletes all user tokens and revokes all ciphers.
// This is a destructive operation and should only be used
// as a last resort, for example, if the database encryption key has been
@@ -256,9 +327,9 @@ func Delete(ctx context.Context, log slog.Logger, sqlDB *sql.DB) error {
store := database.New(sqlDB)
_, err := sqlDB.ExecContext(ctx, sqlDeleteEncryptedUserTokens)
if err != nil {
return xerrors.Errorf("delete encrypted tokens and chat provider keys: %w", err)
return xerrors.Errorf("delete encrypted tokens, chat provider keys, and chat automation webhook secrets: %w", err)
}
log.Info(ctx, "deleted encrypted user tokens and chat provider API keys")
log.Info(ctx, "deleted encrypted user tokens, chat provider API keys, and chat automation webhook secrets")
log.Info(ctx, "revoking all active keys")
keys, err := store.GetDBCryptKeys(ctx)
+67
View File
@@ -385,6 +385,73 @@ func (db *dbCrypt) GetCryptoKeysByFeature(ctx context.Context, feature database.
return keys, nil
}
func (db *dbCrypt) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
trigger, err := db.Store.GetChatAutomationTriggerByID(ctx, id)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := db.decryptField(&trigger.WebhookSecret.String, trigger.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
return trigger, nil
}
func (db *dbCrypt) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
triggers, err := db.Store.GetChatAutomationTriggersByAutomationID(ctx, automationID)
if err != nil {
return nil, err
}
for i := range triggers {
if err := db.decryptField(&triggers[i].WebhookSecret.String, triggers[i].WebhookSecretKeyID); err != nil {
return nil, err
}
}
return triggers, nil
}
func (db *dbCrypt) InsertChatAutomationTrigger(ctx context.Context, params database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
if !params.WebhookSecret.Valid || strings.TrimSpace(params.WebhookSecret.String) == "" {
params.WebhookSecretKeyID = sql.NullString{}
} else if err := db.encryptField(&params.WebhookSecret.String, &params.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
trigger, err := db.Store.InsertChatAutomationTrigger(ctx, params)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := db.decryptField(&trigger.WebhookSecret.String, trigger.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
return trigger, nil
}
func (db *dbCrypt) UpdateChatAutomationTrigger(ctx context.Context, params database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
trigger, err := db.Store.UpdateChatAutomationTrigger(ctx, params)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := db.decryptField(&trigger.WebhookSecret.String, trigger.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
return trigger, nil
}
func (db *dbCrypt) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, params database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
if !params.WebhookSecret.Valid || strings.TrimSpace(params.WebhookSecret.String) == "" {
params.WebhookSecretKeyID = sql.NullString{}
} else if err := db.encryptField(&params.WebhookSecret.String, &params.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
trigger, err := db.Store.UpdateChatAutomationTriggerWebhookSecret(ctx, params)
if err != nil {
return database.ChatAutomationTrigger{}, err
}
if err := db.decryptField(&trigger.WebhookSecret.String, trigger.WebhookSecretKeyID); err != nil {
return database.ChatAutomationTrigger{}, err
}
return trigger, nil
}
func (db *dbCrypt) GetChatProviderByID(ctx context.Context, id uuid.UUID) (database.ChatProvider, error) {
provider, err := db.Store.GetChatProviderByID(ctx, id)
if err != nil {
+193
View File
@@ -1177,3 +1177,196 @@ func TestMCPServerUserTokens(t *testing.T) {
requireEncryptedEquals(t, ciphers[0], rawTok.RefreshToken, refreshToken)
})
}
func TestChatAutomationTriggers(t *testing.T) {
t.Parallel()
ctx := context.Background()
const (
//nolint:gosec // test credential
webhookSecret = "whsec-test-secret-value"
)
// insertTrigger creates a user, organization, chat automation, and
// a webhook trigger with a secret through the encrypted store.
insertTrigger := func(t *testing.T, crypt *dbCrypt, ciphers []Cipher) database.ChatAutomationTrigger {
t.Helper()
user := dbgen.User(t, crypt, database.User{})
org := dbgen.Organization(t, crypt, database.Organization{})
now := dbtime.Now()
automation, err := crypt.InsertChatAutomation(ctx, database.InsertChatAutomationParams{
ID: uuid.New(),
OwnerID: user.ID,
OrganizationID: org.ID,
Name: "test-automation-" + uuid.New().String()[:8],
Description: "test automation",
Instructions: "do stuff",
MCPServerIDs: []uuid.UUID{},
AllowedTools: []string{},
Status: database.ChatAutomationStatusActive,
MaxChatCreatesPerHour: 10,
MaxMessagesPerHour: 60,
CreatedAt: now,
UpdatedAt: now,
})
require.NoError(t, err)
trigger, err := crypt.InsertChatAutomationTrigger(ctx, database.InsertChatAutomationTriggerParams{
ID: uuid.New(),
AutomationID: automation.ID,
Type: database.ChatAutomationTriggerTypeWebhook,
WebhookSecret: sql.NullString{String: webhookSecret, Valid: true},
CreatedAt: now,
UpdatedAt: now,
})
require.NoError(t, err)
require.Equal(t, webhookSecret, trigger.WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), trigger.WebhookSecretKeyID.String)
return trigger
}
// requireTriggerRawEncrypted reads the trigger from the raw
// (unwrapped) store and asserts the secret field is encrypted.
requireTriggerRawEncrypted := func(
t *testing.T,
rawDB database.Store,
triggerID uuid.UUID,
ciphers []Cipher,
wantSecret string,
) {
t.Helper()
raw, err := rawDB.GetChatAutomationTriggerByID(ctx, triggerID)
require.NoError(t, err)
requireEncryptedEquals(t, ciphers[0], raw.WebhookSecret.String, wantSecret)
}
t.Run("InsertChatAutomationTrigger", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
requireTriggerRawEncrypted(t, db, trigger.ID, ciphers, webhookSecret)
})
t.Run("GetChatAutomationTriggerByID", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
got, err := crypt.GetChatAutomationTriggerByID(ctx, trigger.ID)
require.NoError(t, err)
require.Equal(t, webhookSecret, got.WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), got.WebhookSecretKeyID.String)
requireTriggerRawEncrypted(t, db, trigger.ID, ciphers, webhookSecret)
})
t.Run("GetChatAutomationTriggersByAutomationID", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
triggers, err := crypt.GetChatAutomationTriggersByAutomationID(ctx, trigger.AutomationID)
require.NoError(t, err)
require.Len(t, triggers, 1)
require.Equal(t, webhookSecret, triggers[0].WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), triggers[0].WebhookSecretKeyID.String)
requireTriggerRawEncrypted(t, db, trigger.ID, ciphers, webhookSecret)
})
t.Run("UpdateChatAutomationTrigger", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
// UpdateChatAutomationTrigger does not change the webhook
// secret itself; it updates cron_schedule/filter/label_paths.
// The returned trigger should still have the decrypted secret.
updated, err := crypt.UpdateChatAutomationTrigger(ctx, database.UpdateChatAutomationTriggerParams{
ID: trigger.ID,
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
require.Equal(t, webhookSecret, updated.WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), updated.WebhookSecretKeyID.String)
requireTriggerRawEncrypted(t, db, trigger.ID, ciphers, webhookSecret)
})
t.Run("UpdateChatAutomationTriggerWebhookSecret", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
const newSecret = "whsec-rotated-secret" //nolint:gosec // test credential
updated, err := crypt.UpdateChatAutomationTriggerWebhookSecret(ctx, database.UpdateChatAutomationTriggerWebhookSecretParams{
ID: trigger.ID,
WebhookSecret: sql.NullString{String: newSecret, Valid: true},
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
require.Equal(t, newSecret, updated.WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), updated.WebhookSecretKeyID.String)
requireTriggerRawEncrypted(t, db, trigger.ID, ciphers, newSecret)
})
t.Run("CronTriggerThroughDecryptLoop", func(t *testing.T) {
t.Parallel()
_, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
// Insert a cron trigger under the same automation. Cron
// triggers have no webhook secret, so the secret fields
// should remain NULL through the encrypt/decrypt loop.
now := dbtime.Now()
cronTrigger, err := crypt.InsertChatAutomationTrigger(ctx, database.InsertChatAutomationTriggerParams{
ID: uuid.New(),
AutomationID: trigger.AutomationID,
Type: database.ChatAutomationTriggerTypeCron,
CronSchedule: sql.NullString{String: "0 * * * *", Valid: true},
WebhookSecret: sql.NullString{Valid: false},
CreatedAt: now,
UpdatedAt: now,
})
require.NoError(t, err)
require.False(t, cronTrigger.WebhookSecret.Valid)
require.False(t, cronTrigger.WebhookSecretKeyID.Valid)
// Fetch both triggers by automation ID and verify each
// comes back with the expected secret state.
triggers, err := crypt.GetChatAutomationTriggersByAutomationID(ctx, trigger.AutomationID)
require.NoError(t, err)
require.Len(t, triggers, 2)
for _, tr := range triggers {
switch tr.Type {
case database.ChatAutomationTriggerTypeWebhook:
require.Equal(t, webhookSecret, tr.WebhookSecret.String)
require.Equal(t, ciphers[0].HexDigest(), tr.WebhookSecretKeyID.String)
case database.ChatAutomationTriggerTypeCron:
require.False(t, tr.WebhookSecret.Valid, "cron trigger should have NULL secret")
require.False(t, tr.WebhookSecretKeyID.Valid, "cron trigger should have NULL key_id")
require.Equal(t, "0 * * * *", tr.CronSchedule.String)
default:
t.Fatalf("unexpected trigger type: %s", tr.Type)
}
}
})
t.Run("ClearWebhookSecretToNULL", func(t *testing.T) {
t.Parallel()
_, crypt, ciphers := setup(t)
trigger := insertTrigger(t, crypt, ciphers)
// The DB schema enforces that webhook-type triggers must
// always have a non-NULL webhook_secret via the
// chat_automation_triggers_webhook_fields constraint.
// Attempting to clear it to NULL should fail. This verifies
// the dbcrypt layer correctly clears the key_id and passes
// the NULL through to the DB, which then rejects it.
_, err := crypt.UpdateChatAutomationTriggerWebhookSecret(ctx, database.UpdateChatAutomationTriggerWebhookSecretParams{
ID: trigger.ID,
WebhookSecret: sql.NullString{Valid: false},
UpdatedAt: dbtime.Now(),
})
require.Error(t, err)
require.Contains(t, err.Error(), "chat_automation_triggers_webhook_fields")
})
}
+6
View File
@@ -47,6 +47,12 @@ export const RBACResourceActions: Partial<
read: "read chat messages and metadata",
update: "update chat title or settings",
},
chat_automation: {
create: "create a chat automation",
delete: "delete a chat automation",
read: "read chat automation configuration",
update: "update a chat automation",
},
connection_log: {
read: "read connection logs",
update: "upsert connection log entries",
+12
View File
@@ -320,6 +320,11 @@ export type APIKeyScope =
| "boundary_usage:read"
| "boundary_usage:update"
| "chat:*"
| "chat_automation:*"
| "chat_automation:create"
| "chat_automation:delete"
| "chat_automation:read"
| "chat_automation:update"
| "chat:create"
| "chat:delete"
| "chat:read"
@@ -529,6 +534,11 @@ export const APIKeyScopes: APIKeyScope[] = [
"boundary_usage:read",
"boundary_usage:update",
"chat:*",
"chat_automation:*",
"chat_automation:create",
"chat_automation:delete",
"chat_automation:read",
"chat_automation:update",
"chat:create",
"chat:delete",
"chat:read",
@@ -5585,6 +5595,7 @@ export type RBACResource =
| "audit_log"
| "boundary_usage"
| "chat"
| "chat_automation"
| "connection_log"
| "crypto_key"
| "debug_info"
@@ -5631,6 +5642,7 @@ export const RBACResources: RBACResource[] = [
"audit_log",
"boundary_usage",
"chat",
"chat_automation",
"connection_log",
"crypto_key",
"debug_info",