Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 459ea75c57 | |||
| c04f4f6abf | |||
| b117acfaaf | |||
| 8d6143bbef | |||
| c6f384cc94 | |||
| 17e73f1711 | |||
| 39a5af04bf | |||
| 13f77b2b27 | |||
| 13ab3ad058 |
+6
-2
@@ -63,6 +63,8 @@ import (
|
||||
"github.com/coder/coder/v2/cli/config"
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/autobuild"
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
"github.com/coder/coder/v2/coderd/automations/cronscheduler"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/awsiamrds"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
@@ -1157,8 +1159,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
jobReaper.Start()
|
||||
defer jobReaper.Close()
|
||||
|
||||
waitForProvisionerJobs := false
|
||||
// Currently there is no way to ask the server to shut
|
||||
// Evaluates cron-based automation triggers every minute.
|
||||
automationCron := cronscheduler.New(ctx, logger.Named("automation_cron"), options.Database, quartz.NewReal(), &automations.ChatdAdapter{Server: coderAPI.ChatDaemon()})
|
||||
defer automationCron.Close()
|
||||
waitForProvisionerJobs := false // Currently there is no way to ask the server to shut
|
||||
// itself down, so any exit signal will result in a non-zero
|
||||
// exit of the server.
|
||||
var exitErr error
|
||||
|
||||
Generated
+12
@@ -13184,6 +13184,11 @@ const docTemplate = `{
|
||||
"audit_log:*",
|
||||
"audit_log:create",
|
||||
"audit_log:read",
|
||||
"automation:*",
|
||||
"automation:create",
|
||||
"automation:delete",
|
||||
"automation:read",
|
||||
"automation:update",
|
||||
"boundary_usage:*",
|
||||
"boundary_usage:delete",
|
||||
"boundary_usage:read",
|
||||
@@ -13393,6 +13398,11 @@ const docTemplate = `{
|
||||
"APIKeyScopeAuditLogAll",
|
||||
"APIKeyScopeAuditLogCreate",
|
||||
"APIKeyScopeAuditLogRead",
|
||||
"APIKeyScopeAutomationAll",
|
||||
"APIKeyScopeAutomationCreate",
|
||||
"APIKeyScopeAutomationDelete",
|
||||
"APIKeyScopeAutomationRead",
|
||||
"APIKeyScopeAutomationUpdate",
|
||||
"APIKeyScopeBoundaryUsageAll",
|
||||
"APIKeyScopeBoundaryUsageDelete",
|
||||
"APIKeyScopeBoundaryUsageRead",
|
||||
@@ -18780,6 +18790,7 @@ const docTemplate = `{
|
||||
"assign_org_role",
|
||||
"assign_role",
|
||||
"audit_log",
|
||||
"automation",
|
||||
"boundary_usage",
|
||||
"chat",
|
||||
"connection_log",
|
||||
@@ -18826,6 +18837,7 @@ const docTemplate = `{
|
||||
"ResourceAssignOrgRole",
|
||||
"ResourceAssignRole",
|
||||
"ResourceAuditLog",
|
||||
"ResourceAutomation",
|
||||
"ResourceBoundaryUsage",
|
||||
"ResourceChat",
|
||||
"ResourceConnectionLog",
|
||||
|
||||
Generated
+12
@@ -11762,6 +11762,11 @@
|
||||
"audit_log:*",
|
||||
"audit_log:create",
|
||||
"audit_log:read",
|
||||
"automation:*",
|
||||
"automation:create",
|
||||
"automation:delete",
|
||||
"automation:read",
|
||||
"automation:update",
|
||||
"boundary_usage:*",
|
||||
"boundary_usage:delete",
|
||||
"boundary_usage:read",
|
||||
@@ -11971,6 +11976,11 @@
|
||||
"APIKeyScopeAuditLogAll",
|
||||
"APIKeyScopeAuditLogCreate",
|
||||
"APIKeyScopeAuditLogRead",
|
||||
"APIKeyScopeAutomationAll",
|
||||
"APIKeyScopeAutomationCreate",
|
||||
"APIKeyScopeAutomationDelete",
|
||||
"APIKeyScopeAutomationRead",
|
||||
"APIKeyScopeAutomationUpdate",
|
||||
"APIKeyScopeBoundaryUsageAll",
|
||||
"APIKeyScopeBoundaryUsageDelete",
|
||||
"APIKeyScopeBoundaryUsageRead",
|
||||
@@ -17160,6 +17170,7 @@
|
||||
"assign_org_role",
|
||||
"assign_role",
|
||||
"audit_log",
|
||||
"automation",
|
||||
"boundary_usage",
|
||||
"chat",
|
||||
"connection_log",
|
||||
@@ -17206,6 +17217,7 @@
|
||||
"ResourceAssignOrgRole",
|
||||
"ResourceAssignRole",
|
||||
"ResourceAuditLog",
|
||||
"ResourceAutomation",
|
||||
"ResourceBoundaryUsage",
|
||||
"ResourceChat",
|
||||
"ResourceConnectionLog",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
chatd "github.com/coder/coder/v2/coderd/x/chatd"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// ChatdAdapter implements ChatCreator by delegating to chatd.Server.
|
||||
type ChatdAdapter struct {
|
||||
Server *chatd.Server
|
||||
}
|
||||
|
||||
// CreateChat creates a new chat through the chatd server and returns
|
||||
// the chat ID.
|
||||
func (a *ChatdAdapter) CreateChat(ctx context.Context, opts CreateChatOptions) (uuid.UUID, error) {
|
||||
var modelConfigID uuid.UUID
|
||||
if opts.ModelConfigID.Valid {
|
||||
modelConfigID = opts.ModelConfigID.UUID
|
||||
}
|
||||
labels := database.StringMap{}
|
||||
for k, v := range opts.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
chat, err := a.Server.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: opts.OwnerID,
|
||||
Title: opts.Title,
|
||||
ModelConfigID: modelConfigID,
|
||||
MCPServerIDs: opts.MCPServerIDs,
|
||||
Labels: labels,
|
||||
InitialUserContent: []codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText(opts.Instructions),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return chat.ID, nil
|
||||
}
|
||||
|
||||
// SendMessage appends a user message to an existing chat.
|
||||
func (a *ChatdAdapter) SendMessage(ctx context.Context, chatID uuid.UUID, ownerID uuid.UUID, content string) error {
|
||||
_, err := a.Server.SendMessage(ctx, chatd.SendMessageOptions{
|
||||
ChatID: chatID,
|
||||
CreatedBy: ownerID,
|
||||
Content: []codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText(content),
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package cronscheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/pproflabel"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
const tickInterval = time.Minute
|
||||
|
||||
// New creates a background scheduler that evaluates cron-based
|
||||
// automation triggers every minute. Only one replica runs the
|
||||
// scheduler at a time via an advisory lock.
|
||||
func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.Clock, chat automations.ChatCreator) io.Closer {
|
||||
closed := make(chan struct{})
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
//nolint:gocritic // System-level background job needs broad read access.
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
inst := &instance{
|
||||
cancel: cancelFunc,
|
||||
closed: closed,
|
||||
logger: logger,
|
||||
db: db,
|
||||
clk: clk,
|
||||
chat: chat,
|
||||
}
|
||||
|
||||
ticker := clk.NewTicker(tickInterval)
|
||||
doTick := func(ctx context.Context, now time.Time) {
|
||||
defer ticker.Reset(tickInterval)
|
||||
inst.tick(ctx, now)
|
||||
}
|
||||
|
||||
pproflabel.Go(ctx, pproflabel.Service("automation-cron"), func(ctx context.Context) {
|
||||
defer close(closed)
|
||||
defer ticker.Stop()
|
||||
// Force an initial tick.
|
||||
doTick(ctx, dbtime.Time(clk.Now()).UTC())
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
ticker.Stop()
|
||||
doTick(ctx, dbtime.Time(t).UTC())
|
||||
}
|
||||
}
|
||||
})
|
||||
return inst
|
||||
}
|
||||
|
||||
type instance struct {
|
||||
cancel context.CancelFunc
|
||||
closed chan struct{}
|
||||
logger slog.Logger
|
||||
db database.Store
|
||||
clk quartz.Clock
|
||||
chat automations.ChatCreator
|
||||
}
|
||||
|
||||
func (i *instance) Close() error {
|
||||
i.cancel()
|
||||
<-i.closed
|
||||
return nil
|
||||
}
|
||||
|
||||
// tick runs a single scheduler iteration. It acquires an advisory
|
||||
// lock so that only one replica processes cron triggers at a time.
|
||||
func (i *instance) tick(ctx context.Context, now time.Time) {
|
||||
err := i.db.InTx(func(tx database.Store) error {
|
||||
// Only one replica should evaluate cron triggers.
|
||||
ok, err := tx.TryAcquireLock(ctx, database.LockIDAutomationCron)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
i.logger.Debug(ctx, "unable to acquire automation cron lock, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
triggers, err := tx.GetActiveCronTriggers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range triggers {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
i.processTrigger(ctx, tx, t, now)
|
||||
}
|
||||
return nil
|
||||
}, database.DefaultTXOptions().WithID("automation_cron"))
|
||||
if err != nil && ctx.Err() == nil {
|
||||
i.logger.Error(ctx, "automation cron tick failed", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// processTrigger evaluates a single cron trigger and fires it if
|
||||
// the schedule indicates it is due.
|
||||
func (i *instance) processTrigger(ctx context.Context, db database.Store, trigger database.GetActiveCronTriggersRow, now time.Time) {
|
||||
logger := i.logger.With(
|
||||
slog.F("trigger_id", trigger.ID),
|
||||
slog.F("automation_id", trigger.AutomationID),
|
||||
)
|
||||
|
||||
if !trigger.CronSchedule.Valid {
|
||||
return
|
||||
}
|
||||
|
||||
sched, err := cron.Standard(trigger.CronSchedule.String)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "invalid cron schedule on trigger",
|
||||
slog.F("schedule", trigger.CronSchedule.String),
|
||||
slog.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the reference time for computing "next fire".
|
||||
// If the trigger has never fired, use its creation time.
|
||||
ref := trigger.CreatedAt
|
||||
if trigger.LastTriggeredAt.Valid {
|
||||
ref = trigger.LastTriggeredAt.Time
|
||||
}
|
||||
|
||||
next := sched.Next(ref)
|
||||
if next.After(now) {
|
||||
// Not yet due.
|
||||
return
|
||||
}
|
||||
|
||||
// Build a synthetic payload for the cron event.
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"trigger": "cron",
|
||||
"schedule": trigger.CronSchedule.String,
|
||||
"scheduled_at": next.UTC().Format(time.RFC3339),
|
||||
"fired_at": now.UTC().Format(time.RFC3339),
|
||||
})
|
||||
|
||||
// Resolve labels against the synthetic payload if configured.
|
||||
var resolvedLabels map[string]string
|
||||
if trigger.LabelPaths.Valid {
|
||||
var labelPaths map[string]string
|
||||
if err := json.Unmarshal(trigger.LabelPaths.RawMessage, &labelPaths); err == nil && len(labelPaths) > 0 {
|
||||
resolvedLabels = automations.ResolveLabels(string(payload), labelPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the FireOptions from the trigger row.
|
||||
fireOpts := automations.FireOptions{
|
||||
AutomationID: trigger.AutomationID,
|
||||
AutomationName: trigger.AutomationName,
|
||||
AutomationStatus: trigger.AutomationStatus,
|
||||
AutomationOwnerID: trigger.AutomationOwnerID,
|
||||
AutomationInstructions: trigger.AutomationInstructions,
|
||||
AutomationModelConfigID: trigger.AutomationModelConfigID,
|
||||
AutomationMCPServerIDs: trigger.AutomationMcpServerIds,
|
||||
AutomationAllowedTools: trigger.AutomationAllowedTools,
|
||||
MaxChatCreatesPerHour: trigger.AutomationMaxChatCreatesPerHour,
|
||||
MaxMessagesPerHour: trigger.AutomationMaxMessagesPerHour,
|
||||
TriggerID: trigger.ID,
|
||||
Payload: payload,
|
||||
FilterMatched: true,
|
||||
ResolvedLabels: resolvedLabels,
|
||||
}
|
||||
|
||||
result := automations.Fire(ctx, logger, db, i.chat, fireOpts)
|
||||
|
||||
// Update last_triggered_at so this trigger is not re-fired
|
||||
// until the next scheduled time.
|
||||
updateErr := db.UpdateAutomationTriggerLastTriggeredAt(ctx, database.UpdateAutomationTriggerLastTriggeredAtParams{
|
||||
LastTriggeredAt: now,
|
||||
ID: trigger.ID,
|
||||
})
|
||||
if updateErr != nil {
|
||||
logger.Error(ctx, "failed to update last_triggered_at", slog.Error(updateErr))
|
||||
}
|
||||
|
||||
logger.Info(ctx, "fired cron automation trigger",
|
||||
slog.F("status", result.Status),
|
||||
slog.F("schedule", trigger.CronSchedule.String),
|
||||
slog.F("next_after_ref", next),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package cronscheduler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
"github.com/coder/coder/v2/coderd/automations/cronscheduler"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
||||
}
|
||||
|
||||
// awaitDoTick waits for the scheduler to complete its initial tick.
|
||||
// It traps the quartz.Mock clock events in the same order the
|
||||
// scheduler emits them: Now() → TickerReset.
|
||||
func awaitDoTick(ctx context.Context, t *testing.T, clk *quartz.Mock) chan struct{} {
|
||||
t.Helper()
|
||||
ch := make(chan struct{})
|
||||
trapNow := clk.Trap().Now()
|
||||
trapReset := clk.Trap().TickerReset()
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer trapReset.Close()
|
||||
defer trapNow.Close()
|
||||
// Wait for the initial Now() call that kicks off doTick.
|
||||
trapNow.MustWait(ctx).MustRelease(ctx)
|
||||
// Wait for the ticker reset that signals doTick completed.
|
||||
trapReset.MustWait(ctx).MustRelease(ctx)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// fakeChatCreator is a stub ChatCreator for tests. CreateChat
|
||||
// returns a new UUID; SendMessage is a no-op.
|
||||
type fakeChatCreator struct{}
|
||||
|
||||
func (fakeChatCreator) CreateChat(_ context.Context, _ automations.CreateChatOptions) (uuid.UUID, error) {
|
||||
return uuid.New(), nil
|
||||
}
|
||||
|
||||
func (fakeChatCreator) SendMessage(_ context.Context, _ uuid.UUID, _ uuid.UUID, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeTrigger builds a GetActiveCronTriggersRow with sensible
|
||||
// defaults. Fields can be overridden after creation.
|
||||
func makeTrigger(schedule string, status string, createdAt time.Time) database.GetActiveCronTriggersRow {
|
||||
return database.GetActiveCronTriggersRow{
|
||||
ID: uuid.New(),
|
||||
AutomationID: uuid.New(),
|
||||
Type: "cron",
|
||||
CronSchedule: sql.NullString{String: schedule, Valid: true},
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
AutomationStatus: status,
|
||||
AutomationOwnerID: uuid.New(),
|
||||
AutomationMaxChatCreatesPerHour: 10,
|
||||
AutomationMaxMessagesPerHour: 100,
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:paralleltest // Uses LockIDAutomationCron advisory lock mock.
|
||||
func TestScheduler(t *testing.T) {
|
||||
t.Run("NoTriggers", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
// The scheduler calls InTx; execute the function against
|
||||
// the same mock so inner calls are recorded.
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
).Times(1)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(nil, nil)
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("DueTriggerFires", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
// Trigger was created 2 minutes ago with "every minute"
|
||||
// schedule, so it should fire.
|
||||
trigger := makeTrigger("* * * * *", "active", now.Add(-2*time.Minute))
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(
|
||||
[]database.GetActiveCronTriggersRow{trigger}, nil,
|
||||
)
|
||||
|
||||
// Fire() checks rate limits before creating a chat.
|
||||
mDB.EXPECT().CountAutomationChatCreatesInWindow(gomock.Any(), gomock.Any()).Return(int64(0), nil)
|
||||
mDB.EXPECT().CountAutomationMessagesInWindow(gomock.Any(), gomock.Any()).Return(int64(0), nil)
|
||||
|
||||
// Expect the event to be inserted with status "created".
|
||||
mDB.EXPECT().InsertAutomationEvent(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, arg database.InsertAutomationEventParams) (database.AutomationEvent, error) {
|
||||
assert.Equal(t, trigger.AutomationID, arg.AutomationID)
|
||||
assert.Equal(t, trigger.ID, arg.TriggerID.UUID)
|
||||
assert.Equal(t, "created", arg.Status)
|
||||
assert.True(t, arg.FilterMatched)
|
||||
return database.AutomationEvent{ID: uuid.New()}, nil
|
||||
},
|
||||
)
|
||||
// Expect last_triggered_at to be updated.
|
||||
mDB.EXPECT().UpdateAutomationTriggerLastTriggeredAt(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, arg database.UpdateAutomationTriggerLastTriggeredAtParams) error {
|
||||
assert.Equal(t, trigger.ID, arg.ID)
|
||||
assert.Equal(t, now, arg.LastTriggeredAt)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, fakeChatCreator{})
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("NotYetDueTriggerSkipped", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
// Trigger created now with "every hour" schedule. Next fire
|
||||
// is 1 hour from now, so it should NOT fire.
|
||||
trigger := makeTrigger("0 * * * *", "active", now)
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(
|
||||
[]database.GetActiveCronTriggersRow{trigger}, nil,
|
||||
)
|
||||
// No InsertAutomationEvent or UpdateAutomationTriggerLastTriggeredAt
|
||||
// expected — the trigger is not due.
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("PreviewModeCreatesPreviewEvent", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
trigger := makeTrigger("* * * * *", "preview", now.Add(-2*time.Minute))
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(
|
||||
[]database.GetActiveCronTriggersRow{trigger}, nil,
|
||||
)
|
||||
mDB.EXPECT().InsertAutomationEvent(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, arg database.InsertAutomationEventParams) (database.AutomationEvent, error) {
|
||||
assert.Equal(t, "preview", arg.Status)
|
||||
return database.AutomationEvent{ID: uuid.New()}, nil
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().UpdateAutomationTriggerLastTriggeredAt(gomock.Any(), gomock.Any()).Return(nil)
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("InvalidScheduleSkipped", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
trigger := makeTrigger("not a cron", "active", now.Add(-2*time.Minute))
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(
|
||||
[]database.GetActiveCronTriggersRow{trigger}, nil,
|
||||
)
|
||||
// No event insert expected — invalid schedule is skipped.
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("LastTriggeredAtPreventsRefire", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
now := time.Date(2025, 6, 15, 12, 0, 30, 0, time.UTC)
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
// Trigger with "every hour" schedule that last fired at the
|
||||
// top of this hour. Next fire is 13:00, which is after now.
|
||||
trigger := makeTrigger("0 * * * *", "active", now.Add(-24*time.Hour))
|
||||
trigger.LastTriggeredAt = sql.NullTime{
|
||||
Time: time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(true, nil)
|
||||
mDB.EXPECT().GetActiveCronTriggers(gomock.Any()).Return(
|
||||
[]database.GetActiveCronTriggersRow{trigger}, nil,
|
||||
)
|
||||
// No event insert — last_triggered_at means next fire is
|
||||
// in the future.
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
|
||||
t.Run("LockNotAcquiredSkips", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
clk := quartz.NewMock(t)
|
||||
|
||||
mDB.EXPECT().InTx(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(fn func(database.Store) error, _ *database.TxOptions) error {
|
||||
return fn(mDB)
|
||||
},
|
||||
)
|
||||
// Another replica holds the lock.
|
||||
mDB.EXPECT().TryAcquireLock(gomock.Any(), int64(database.LockIDAutomationCron)).Return(false, nil)
|
||||
// No GetActiveCronTriggers call expected.
|
||||
|
||||
done := awaitDoTick(ctx, t, clk)
|
||||
scheduler := cronscheduler.New(ctx, testutil.Logger(t), mDB, clk, nil)
|
||||
<-done
|
||||
require.NoError(t, scheduler.Close())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
// ChatCreator abstracts the chatd.Server so the automations package
|
||||
// does not depend on it directly. Both the webhook handler and the
|
||||
// cron scheduler inject the real implementation.
|
||||
type ChatCreator interface {
|
||||
// CreateChat creates a new chat and sends the initial message.
|
||||
// The returned UUID is the chat ID.
|
||||
CreateChat(ctx context.Context, opts CreateChatOptions) (uuid.UUID, error)
|
||||
// SendMessage appends a user message to an existing chat.
|
||||
SendMessage(ctx context.Context, chatID uuid.UUID, ownerID uuid.UUID, content string) error
|
||||
}
|
||||
|
||||
// CreateChatOptions contains everything needed to create an
|
||||
// automation-initiated chat.
|
||||
type CreateChatOptions struct {
|
||||
OwnerID uuid.UUID
|
||||
AutomationID uuid.UUID
|
||||
Title string
|
||||
Instructions string
|
||||
ModelConfigID uuid.NullUUID
|
||||
MCPServerIDs []uuid.UUID
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// FireResult is the outcome of an automation trigger firing.
|
||||
type FireResult struct {
|
||||
Status string
|
||||
MatchedChatID uuid.NullUUID
|
||||
CreatedChatID uuid.NullUUID
|
||||
Error string
|
||||
}
|
||||
|
||||
// FireOptions contains the inputs for firing an automation trigger.
|
||||
type FireOptions struct {
|
||||
// Automation fields.
|
||||
AutomationID uuid.UUID
|
||||
AutomationName string
|
||||
AutomationStatus string
|
||||
AutomationOwnerID uuid.UUID
|
||||
AutomationInstructions string
|
||||
AutomationModelConfigID uuid.NullUUID
|
||||
AutomationMCPServerIDs []uuid.UUID
|
||||
AutomationAllowedTools []string
|
||||
MaxChatCreatesPerHour int32
|
||||
MaxMessagesPerHour int32
|
||||
|
||||
// Trigger fields.
|
||||
TriggerID uuid.UUID
|
||||
|
||||
// Resolved data.
|
||||
Payload json.RawMessage
|
||||
FilterMatched bool
|
||||
ResolvedLabels map[string]string
|
||||
}
|
||||
|
||||
// Fire executes the active-mode logic for an automation trigger:
|
||||
// rate-limit check, find-or-create chat, send message, and record
|
||||
// the event. For preview mode it only logs the event without
|
||||
// creating a chat.
|
||||
func Fire(
|
||||
ctx context.Context,
|
||||
logger slog.Logger,
|
||||
db database.Store,
|
||||
chat ChatCreator,
|
||||
opts FireOptions,
|
||||
) FireResult {
|
||||
triggerUUID := uuid.NullUUID{UUID: opts.TriggerID, Valid: true}
|
||||
|
||||
// Resolve labels JSON for the event record.
|
||||
var resolvedLabelsJSON pqtype.NullRawMessage
|
||||
if len(opts.ResolvedLabels) > 0 {
|
||||
if j, err := json.Marshal(opts.ResolvedLabels); err == nil {
|
||||
resolvedLabelsJSON = pqtype.NullRawMessage{RawMessage: j, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Preview mode: log the event, optionally look up a matching
|
||||
// chat, but never create or continue one.
|
||||
if opts.AutomationStatus == "preview" {
|
||||
result := FireResult{Status: "preview"}
|
||||
// Try to find a matching chat for the preview log.
|
||||
if len(opts.ResolvedLabels) > 0 {
|
||||
if chatID, ok := findChatByLabels(ctx, db, opts.AutomationOwnerID, opts.ResolvedLabels); ok {
|
||||
result.MatchedChatID = uuid.NullUUID{UUID: chatID, Valid: true}
|
||||
}
|
||||
}
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
MatchedChatID: result.MatchedChatID,
|
||||
Status: "preview",
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Active mode: enforce rate limits.
|
||||
windowStart := time.Now().Add(-time.Hour)
|
||||
|
||||
chatCreates, err := db.CountAutomationChatCreatesInWindow(ctx, database.CountAutomationChatCreatesInWindowParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
WindowStart: windowStart,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to count chat creates", slog.Error(err))
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
Status: "error",
|
||||
Error: sql.NullString{String: "failed to check rate limits", Valid: true},
|
||||
})
|
||||
return FireResult{Status: "error", Error: "failed to check rate limits"}
|
||||
}
|
||||
|
||||
msgCount, err := db.CountAutomationMessagesInWindow(ctx, database.CountAutomationMessagesInWindowParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
WindowStart: windowStart,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to count messages", slog.Error(err))
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
Status: "error",
|
||||
Error: sql.NullString{String: "failed to check rate limits", Valid: true},
|
||||
})
|
||||
return FireResult{Status: "error", Error: "failed to check rate limits"}
|
||||
}
|
||||
|
||||
// Check message rate limit (applies to both create and continue).
|
||||
if msgCount >= int64(opts.MaxMessagesPerHour) {
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
Status: "rate_limited",
|
||||
Error: sql.NullString{
|
||||
String: fmt.Sprintf("message rate limit exceeded: %d/%d per hour", msgCount, opts.MaxMessagesPerHour),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
return FireResult{Status: "rate_limited", Error: "message rate limit exceeded"}
|
||||
}
|
||||
|
||||
// Try to find an existing chat with matching labels to continue.
|
||||
if len(opts.ResolvedLabels) > 0 {
|
||||
if chatID, ok := findChatByLabels(ctx, db, opts.AutomationOwnerID, opts.ResolvedLabels); ok {
|
||||
// Continue existing chat.
|
||||
if err := chat.SendMessage(ctx, chatID, opts.AutomationOwnerID, opts.AutomationInstructions); err != nil {
|
||||
logger.Error(ctx, "failed to send message to existing chat",
|
||||
slog.F("chat_id", chatID),
|
||||
slog.Error(err),
|
||||
)
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
MatchedChatID: uuid.NullUUID{UUID: chatID, Valid: true},
|
||||
Status: "error",
|
||||
Error: sql.NullString{String: "failed to send message to chat", Valid: true},
|
||||
})
|
||||
return FireResult{Status: "error", Error: "failed to send message"}
|
||||
}
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
MatchedChatID: uuid.NullUUID{UUID: chatID, Valid: true},
|
||||
Status: "continued",
|
||||
})
|
||||
return FireResult{
|
||||
Status: "continued",
|
||||
MatchedChatID: uuid.NullUUID{UUID: chatID, Valid: true},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching chat found — create a new one.
|
||||
// Check chat creation rate limit.
|
||||
if chatCreates >= int64(opts.MaxChatCreatesPerHour) {
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
Status: "rate_limited",
|
||||
Error: sql.NullString{
|
||||
String: fmt.Sprintf("chat creation rate limit exceeded: %d/%d per hour", chatCreates, opts.MaxChatCreatesPerHour),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
return FireResult{Status: "rate_limited", Error: "chat creation rate limit exceeded"}
|
||||
}
|
||||
|
||||
newChatID, err := chat.CreateChat(ctx, CreateChatOptions{
|
||||
OwnerID: opts.AutomationOwnerID,
|
||||
AutomationID: opts.AutomationID,
|
||||
Title: fmt.Sprintf("[%s] %s", opts.AutomationName, time.Now().UTC().Format("2006-01-02 15:04")),
|
||||
Instructions: opts.AutomationInstructions,
|
||||
ModelConfigID: opts.AutomationModelConfigID,
|
||||
MCPServerIDs: opts.AutomationMCPServerIDs,
|
||||
Labels: opts.ResolvedLabels,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to create chat", slog.Error(err))
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
Status: "error",
|
||||
Error: sql.NullString{String: "failed to create chat", Valid: true},
|
||||
})
|
||||
return FireResult{Status: "error", Error: "failed to create chat"}
|
||||
}
|
||||
|
||||
insertEvent(ctx, db, database.InsertAutomationEventParams{
|
||||
AutomationID: opts.AutomationID,
|
||||
TriggerID: triggerUUID,
|
||||
Payload: opts.Payload,
|
||||
FilterMatched: opts.FilterMatched,
|
||||
ResolvedLabels: resolvedLabelsJSON,
|
||||
CreatedChatID: uuid.NullUUID{UUID: newChatID, Valid: true},
|
||||
Status: "created",
|
||||
})
|
||||
return FireResult{
|
||||
Status: "created",
|
||||
CreatedChatID: uuid.NullUUID{UUID: newChatID, Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
// findChatByLabels looks up an existing chat owned by the given user
|
||||
// whose labels match the resolved label set.
|
||||
func findChatByLabels(ctx context.Context, db database.Store, ownerID uuid.UUID, labels map[string]string) (uuid.UUID, bool) {
|
||||
labelsJSON, err := json.Marshal(labels)
|
||||
if err != nil {
|
||||
return uuid.Nil, false
|
||||
}
|
||||
chats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: ownerID,
|
||||
LabelFilter: pqtype.NullRawMessage{
|
||||
RawMessage: labelsJSON,
|
||||
Valid: true,
|
||||
},
|
||||
LimitOpt: 1,
|
||||
})
|
||||
if err != nil || len(chats) == 0 {
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return chats[0].ID, true
|
||||
}
|
||||
|
||||
// insertEvent is a fire-and-forget helper that logs errors but never
|
||||
// fails the caller.
|
||||
func insertEvent(ctx context.Context, db database.Store, params database.InsertAutomationEventParams) {
|
||||
_, _ = db.InsertAutomationEvent(ctx, params)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// MatchFilter evaluates a gjson-based filter against a JSON payload.
|
||||
// If filter is nil or empty, the match succeeds (everything passes).
|
||||
// Each key in the filter is a gjson path; each value is the expected
|
||||
// result. All entries must match for the filter to pass.
|
||||
func MatchFilter(payload string, filter json.RawMessage) bool {
|
||||
if len(filter) == 0 || string(filter) == "null" {
|
||||
return true
|
||||
}
|
||||
|
||||
var conditions map[string]any
|
||||
if err := json.Unmarshal(filter, &conditions); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for path, expected := range conditions {
|
||||
result := gjson.Get(payload, path)
|
||||
if !result.Exists() {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(result.Value(), expected) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package automations_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
)
|
||||
|
||||
func TestMatchFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `{"action":"opened","repository":{"full_name":"coder/coder","private":true},"pull_request":{"number":42},"labels":["bug","urgent"],"sender":{"login":"octocat"}}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filter json.RawMessage
|
||||
want bool
|
||||
}{
|
||||
{"nil filter matches everything", nil, true},
|
||||
{"null filter matches everything", json.RawMessage(`null`), true},
|
||||
{"empty filter matches everything", json.RawMessage(`{}`), true},
|
||||
{"single match", json.RawMessage(`{"action":"opened"}`), true},
|
||||
{"nested match", json.RawMessage(`{"repository.full_name":"coder/coder"}`), true},
|
||||
{"multiple conditions all match", json.RawMessage(`{"action":"opened","repository.full_name":"coder/coder"}`), true},
|
||||
{"value mismatch", json.RawMessage(`{"action":"closed"}`), false},
|
||||
{"path does not exist", json.RawMessage(`{"nonexistent":"value"}`), false},
|
||||
{"one of two conditions fails", json.RawMessage(`{"action":"opened","repository.full_name":"other/repo"}`), false},
|
||||
{"numeric match", json.RawMessage(`{"pull_request.number":42}`), true},
|
||||
{"numeric mismatch", json.RawMessage(`{"pull_request.number":99}`), false},
|
||||
{"invalid filter json", json.RawMessage(`not json`), false},
|
||||
{"boolean match", json.RawMessage(`{"repository.private":true}`), true},
|
||||
{"boolean mismatch", json.RawMessage(`{"repository.private":false}`), false},
|
||||
{"array value match", json.RawMessage(`{"labels":["bug","urgent"]}`), true},
|
||||
{"array value mismatch", json.RawMessage(`{"labels":["bug"]}`), false},
|
||||
{"object value match", json.RawMessage(`{"sender":{"login":"octocat"}}`), true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := automations.MatchFilter(payload, tt.filter)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ResolveLabels extracts label values from a JSON payload using gjson
|
||||
// paths. Each key in labelPaths maps a label name to a gjson path
|
||||
// expression. If a path doesn't match, that label is omitted.
|
||||
func ResolveLabels(payload string, labelPaths map[string]string) map[string]string {
|
||||
if len(labelPaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := make(map[string]string, len(labelPaths))
|
||||
for labelKey, gjsonPath := range labelPaths {
|
||||
result := gjson.Get(payload, gjsonPath)
|
||||
if result.Exists() {
|
||||
labels[labelKey] = result.String()
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package automations_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
)
|
||||
|
||||
func TestResolveLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `{"repository":{"full_name":"coder/coder"},"action":"opened","number":42,"draft":false}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
labelPaths map[string]string
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
"nil label paths returns nil",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"empty label paths returns nil",
|
||||
map[string]string{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"simple path extraction",
|
||||
map[string]string{"repo": "repository.full_name"},
|
||||
map[string]string{"repo": "coder/coder"},
|
||||
},
|
||||
{
|
||||
"multiple paths",
|
||||
map[string]string{
|
||||
"repo": "repository.full_name",
|
||||
"action": "action",
|
||||
},
|
||||
map[string]string{
|
||||
"repo": "coder/coder",
|
||||
"action": "opened",
|
||||
},
|
||||
},
|
||||
{
|
||||
"missing path omitted",
|
||||
map[string]string{
|
||||
"repo": "repository.full_name",
|
||||
"missing": "nonexistent.path",
|
||||
},
|
||||
map[string]string{"repo": "coder/coder"},
|
||||
},
|
||||
{
|
||||
"numeric value coerced to string",
|
||||
map[string]string{"num": "number"},
|
||||
map[string]string{"num": "42"},
|
||||
},
|
||||
{
|
||||
"boolean value coerced to string",
|
||||
map[string]string{"draft": "draft"},
|
||||
map[string]string{"draft": "false"},
|
||||
},
|
||||
{
|
||||
"all paths missing returns empty map",
|
||||
map[string]string{"a": "no.such.path", "b": "also.missing"},
|
||||
map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := automations.ResolveLabels(payload, tt.labelPaths)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VerifySignature checks an HMAC-SHA256 signature in the format used
|
||||
// by GitHub and many other webhook providers: "sha256=<hex-digest>".
|
||||
// The comparison uses constant-time equality to prevent timing attacks.
|
||||
func VerifySignature(payload []byte, secret string, signatureHeader string) bool {
|
||||
if secret == "" || signatureHeader == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
parts := strings.SplitN(signatureHeader, "=", 2)
|
||||
if len(parts) != 2 || parts[0] != "sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
expectedMAC, err := hex.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(payload)
|
||||
actualMAC := mac.Sum(nil)
|
||||
|
||||
return hmac.Equal(actualMAC, expectedMAC)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package automations_test
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
)
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
secret := "test-secret"
|
||||
payload := []byte(`{"action":"opened"}`)
|
||||
|
||||
// Compute valid signature.
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(payload)
|
||||
validSig := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secret string
|
||||
sig string
|
||||
want bool
|
||||
}{
|
||||
{"valid signature", secret, validSig, true},
|
||||
{"wrong secret", "wrong-secret", validSig, false},
|
||||
{"empty signature header", secret, "", false},
|
||||
{"empty secret", "", validSig, false},
|
||||
{"missing sha256 prefix", secret, hex.EncodeToString(mac.Sum(nil)), false},
|
||||
{"wrong prefix", secret, "sha512=" + hex.EncodeToString(mac.Sum(nil)), false},
|
||||
{"invalid hex", secret, "sha256=zzzz", false},
|
||||
{"tampered payload signature", secret, "sha256=" + hex.EncodeToString([]byte("wrong")), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := automations.VerifySignature(payload, tt.secret, tt.sig)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1149,6 +1149,31 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
})
|
||||
// Experimental(agents): automation API routes gated by ExperimentAgents.
|
||||
r.Route("/automations", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents),
|
||||
)
|
||||
r.Post("/", api.postAutomation)
|
||||
r.Get("/", api.listAutomations)
|
||||
r.Route("/{automation}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractAutomationParam(options.Database))
|
||||
r.Get("/", api.getAutomation)
|
||||
r.Patch("/", api.patchAutomation)
|
||||
r.Delete("/", api.deleteAutomation)
|
||||
r.Get("/events", api.listAutomationEvents)
|
||||
r.Post("/test", api.testAutomation)
|
||||
r.Route("/triggers", func(r chi.Router) {
|
||||
r.Post("/", api.postAutomationTrigger)
|
||||
r.Get("/", api.listAutomationTriggers)
|
||||
r.Route("/{trigger}", func(r chi.Router) {
|
||||
r.Delete("/", api.deleteAutomationTrigger)
|
||||
r.Post("/regenerate-secret", api.regenerateAutomationTriggerSecret)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
// Experimental(agents): chat API routes gated by ExperimentAgents.
|
||||
r.Route("/chats", func(r chi.Router) {
|
||||
r.Use(
|
||||
@@ -1299,6 +1324,11 @@ func New(options *Options) *API {
|
||||
// r.Use(apiKeyMiddleware)
|
||||
r.Get("/", api.derpMapUpdates)
|
||||
})
|
||||
// Unauthenticated webhook endpoint for automation triggers.
|
||||
// Authentication is via HMAC signature, not API key.
|
||||
r.Route("/automations/triggers/{trigger_id}/webhook", func(r chi.Router) {
|
||||
r.Post("/", api.postAutomationWebhook)
|
||||
})
|
||||
r.Route("/deployment", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/config", api.deploymentValues)
|
||||
@@ -2112,6 +2142,12 @@ type API struct {
|
||||
ProfileCollecting atomic.Bool
|
||||
}
|
||||
|
||||
// ChatDaemon returns the chatd server used for automation-initiated
|
||||
// chat creation and messaging.
|
||||
func (api *API) ChatDaemon() *chatd.Server {
|
||||
return api.chatDaemon
|
||||
}
|
||||
|
||||
// Close waits for all WebSocket connections to drain before returning.
|
||||
func (api *API) Close() error {
|
||||
select {
|
||||
|
||||
@@ -7,6 +7,11 @@ type CheckConstraint string
|
||||
// CheckConstraint enums.
|
||||
const (
|
||||
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
|
||||
CheckAutomationEventsStatusCheck CheckConstraint = "automation_events_status_check" // automation_events
|
||||
CheckAutomationTriggersTypeCheck CheckConstraint = "automation_triggers_type_check" // automation_triggers
|
||||
CheckAutomationsMaxChatCreatesPerHourCheck CheckConstraint = "automations_max_chat_creates_per_hour_check" // automations
|
||||
CheckAutomationsMaxMessagesPerHourCheck CheckConstraint = "automations_max_messages_per_hour_check" // automations
|
||||
CheckAutomationsStatusCheck CheckConstraint = "automations_status_check" // 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
|
||||
|
||||
@@ -1317,3 +1317,80 @@ func ChatDiffStatus(chatID uuid.UUID, status *database.ChatDiffStatus) codersdk.
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Automation converts a database Automation to a codersdk Automation.
|
||||
func Automation(a database.Automation) codersdk.Automation {
|
||||
result := codersdk.Automation{
|
||||
ID: a.ID,
|
||||
OwnerID: a.OwnerID,
|
||||
OrganizationID: a.OrganizationID,
|
||||
Name: a.Name,
|
||||
Description: a.Description,
|
||||
Instructions: a.Instructions,
|
||||
MCPServerIDs: a.MCPServerIDs,
|
||||
AllowedTools: a.AllowedTools,
|
||||
Status: codersdk.AutomationStatus(a.Status),
|
||||
MaxChatCreatesPerHour: a.MaxChatCreatesPerHour,
|
||||
MaxMessagesPerHour: a.MaxMessagesPerHour,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
if a.ModelConfigID.Valid {
|
||||
result.ModelConfigID = &a.ModelConfigID.UUID
|
||||
}
|
||||
if a.MCPServerIDs == nil {
|
||||
result.MCPServerIDs = []uuid.UUID{}
|
||||
}
|
||||
if a.AllowedTools == nil {
|
||||
result.AllowedTools = []string{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AutomationEvent converts a database AutomationEvent to a codersdk
|
||||
// AutomationEvent.
|
||||
func AutomationEvent(e database.AutomationEvent) codersdk.AutomationEvent {
|
||||
result := codersdk.AutomationEvent{
|
||||
ID: e.ID,
|
||||
AutomationID: e.AutomationID,
|
||||
ReceivedAt: e.ReceivedAt,
|
||||
Payload: e.Payload,
|
||||
FilterMatched: e.FilterMatched,
|
||||
ResolvedLabels: e.ResolvedLabels.RawMessage,
|
||||
Status: codersdk.AutomationEventStatus(e.Status),
|
||||
}
|
||||
if e.TriggerID.Valid {
|
||||
result.TriggerID = &e.TriggerID.UUID
|
||||
}
|
||||
if e.MatchedChatID.Valid {
|
||||
result.MatchedChatID = &e.MatchedChatID.UUID
|
||||
}
|
||||
if e.CreatedChatID.Valid {
|
||||
result.CreatedChatID = &e.CreatedChatID.UUID
|
||||
}
|
||||
if e.Error.Valid {
|
||||
result.Error = &e.Error.String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AutomationTrigger converts a database AutomationTrigger to a codersdk
|
||||
// AutomationTrigger.
|
||||
func AutomationTrigger(t database.AutomationTrigger, accessURL string) codersdk.AutomationTrigger {
|
||||
result := codersdk.AutomationTrigger{
|
||||
ID: t.ID,
|
||||
AutomationID: t.AutomationID,
|
||||
Type: codersdk.AutomationTriggerType(t.Type),
|
||||
Filter: t.Filter.RawMessage,
|
||||
LabelPaths: t.LabelPaths.RawMessage,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
if t.Type == "webhook" {
|
||||
result.WebhookURL = accessURL + "/api/v2/automations/triggers/" + t.ID.String() + "/webhook"
|
||||
}
|
||||
if t.CronSchedule.Valid {
|
||||
result.CronSchedule = &t.CronSchedule.String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -454,6 +454,7 @@ var (
|
||||
rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceAutomation.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
}),
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{},
|
||||
@@ -705,6 +706,7 @@ var (
|
||||
DisplayName: "Chat Daemon",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceAutomation.Type: {policy.ActionRead},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate},
|
||||
rbac.ResourceDeploymentConfig.Type: {policy.ActionRead},
|
||||
rbac.ResourceUser.Type: {policy.ActionReadPersonal},
|
||||
@@ -1731,6 +1733,28 @@ func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLog
|
||||
return q.db.CountAuthorizedAuditLogs(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) CountAutomationChatCreatesInWindow(ctx context.Context, arg database.CountAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
automation, err := q.db.GetAutomationByID(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.CountAutomationChatCreatesInWindow(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) CountAutomationMessagesInWindow(ctx context.Context, arg database.CountAutomationMessagesInWindowParams) (int64, error) {
|
||||
automation, err := q.db.GetAutomationByID(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.CountAutomationMessagesInWindow(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 +1866,25 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u
|
||||
return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
return fetchAndExec(q.log, q.auth, policy.ActionDelete, q.db.GetAutomationByID, q.db.DeleteAutomationByID)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
trigger, err := q.db.GetAutomationTriggerByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
automation, err := q.db.GetAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteAutomationTriggerByID(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 +2429,13 @@ func (q *querier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
|
||||
return q.db.GetActiveAISeatCount(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetActiveCronTriggers(ctx context.Context) ([]database.GetActiveCronTriggersRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAutomation.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetActiveCronTriggers(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 +2527,55 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
|
||||
return q.db.GetAuthorizationUserRoles(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetAutomationByID(ctx context.Context, id uuid.UUID) (database.Automation, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAutomationByID)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAutomationEvents(ctx context.Context, arg database.GetAutomationEventsParams) ([]database.AutomationEvent, error) {
|
||||
automation, err := q.db.GetAutomationByID(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.GetAutomationEvents(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.AutomationTrigger, error) {
|
||||
trigger, err := q.db.GetAutomationTriggerByID(ctx, id)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.AutomationTrigger, error) {
|
||||
automation, err := q.db.GetAutomationByID(ctx, automationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAutomationTriggersByAutomationID(ctx, automationID)
|
||||
}
|
||||
|
||||
func (q *querier) GetAutomations(ctx context.Context, arg database.GetAutomationsParams) ([]database.Automation, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAutomation.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
|
||||
}
|
||||
return q.db.GetAuthorizedAutomations(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)
|
||||
}
|
||||
@@ -4717,6 +4816,32 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
|
||||
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAutomation(ctx context.Context, arg database.InsertAutomationParams) (database.Automation, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertAutomation)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAutomationEvent(ctx context.Context, arg database.InsertAutomationEventParams) (database.AutomationEvent, error) {
|
||||
automation, err := q.db.GetAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return database.AutomationEvent{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.AutomationEvent{}, err
|
||||
}
|
||||
return q.db.InsertAutomationEvent(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAutomationTrigger(ctx context.Context, arg database.InsertAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
automation, err := q.db.GetAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
return q.db.InsertAutomationTrigger(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg)
|
||||
}
|
||||
@@ -5484,6 +5609,13 @@ func (q *querier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (d
|
||||
return q.db.PopNextQueuedMessage(ctx, chatID)
|
||||
}
|
||||
|
||||
func (q *querier) PurgeOldAutomationEvents(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAutomation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.PurgeOldAutomationEvents(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
@@ -5619,6 +5751,62 @@ 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) UpdateAutomation(ctx context.Context, arg database.UpdateAutomationParams) (database.Automation, error) {
|
||||
automation, err := q.db.GetAutomationByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Automation{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.Automation{}, err
|
||||
}
|
||||
return q.db.UpdateAutomation(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAutomationTrigger(ctx context.Context, arg database.UpdateAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
trigger, err := q.db.GetAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
return q.db.UpdateAutomationTrigger(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateAutomationTriggerLastTriggeredAtParams) error {
|
||||
trigger, err := q.db.GetAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
automation, err := q.db.GetAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpdateAutomationTriggerLastTriggeredAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateAutomationTriggerWebhookSecretParams) (database.AutomationTrigger, error) {
|
||||
trigger, err := q.db.GetAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.AutomationTrigger{}, err
|
||||
}
|
||||
return q.db.UpdateAutomationTriggerWebhookSecret(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
@@ -7178,3 +7366,7 @@ func (q *querier) CountAuthorizedAIBridgeSessions(ctx context.Context, arg datab
|
||||
func (q *querier) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, _ rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
return q.GetChats(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAuthorizedAutomations(ctx context.Context, arg database.GetAutomationsParams, _ rbac.PreparedAuthorized) ([]database.Automation, error) {
|
||||
return q.GetAutomations(ctx, arg)
|
||||
}
|
||||
|
||||
@@ -1141,6 +1141,125 @@ func (s *MethodTestSuite) TestChats() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestAutomations() {
|
||||
s.Run("InsertAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
arg := testutil.Fake(s.T(), faker, database.InsertAutomationParams{})
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{OwnerID: arg.OwnerID, OrganizationID: arg.OrganizationID})
|
||||
dbm.EXPECT().InsertAutomation(gomock.Any(), arg).Return(automation, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), policy.ActionCreate).Returns(automation)
|
||||
}))
|
||||
s.Run("GetAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns(automation)
|
||||
}))
|
||||
s.Run("GetAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetAutomationsParams{}
|
||||
dbm.EXPECT().GetAuthorizedAutomations(gomock.Any(), params, gomock.Any()).Return([]database.Automation{}, nil).AnyTimes()
|
||||
check.Args(params).Asserts()
|
||||
}))
|
||||
s.Run("UpdateAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := database.UpdateAutomationParams{ID: automation.ID, Name: "Updated"}
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateAutomation(gomock.Any(), arg).Return(automation, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(automation)
|
||||
}))
|
||||
|
||||
s.Run("DeleteAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteAutomationByID(gomock.Any(), automation.ID).Return(nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("InsertAutomationEvent", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := testutil.Fake(s.T(), faker, database.InsertAutomationEventParams{AutomationID: automation.ID})
|
||||
event := testutil.Fake(s.T(), faker, database.AutomationEvent{})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().InsertAutomationEvent(gomock.Any(), arg).Return(event, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(event)
|
||||
}))
|
||||
s.Run("GetAutomationEvents", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := database.GetAutomationEventsParams{AutomationID: automation.ID}
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationEvents(gomock.Any(), arg).Return([]database.AutomationEvent{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns([]database.AutomationEvent{})
|
||||
}))
|
||||
s.Run("CountAutomationChatCreatesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := testutil.Fake(s.T(), faker, database.CountAutomationChatCreatesInWindowParams{AutomationID: automation.ID})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().CountAutomationChatCreatesInWindow(gomock.Any(), arg).Return(int64(0), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(0))
|
||||
}))
|
||||
s.Run("CountAutomationMessagesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := testutil.Fake(s.T(), faker, database.CountAutomationMessagesInWindowParams{AutomationID: automation.ID})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().CountAutomationMessagesInWindow(gomock.Any(), arg).Return(int64(0), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(0))
|
||||
}))
|
||||
s.Run("PurgeOldAutomationEvents", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().PurgeOldAutomationEvents(gomock.Any()).Return(nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceAutomation, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("InsertAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
arg := database.InsertAutomationTriggerParams{AutomationID: automation.ID, Type: "webhook"}
|
||||
trigger := testutil.Fake(s.T(), faker, database.AutomationTrigger{AutomationID: automation.ID})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().InsertAutomationTrigger(gomock.Any(), arg).Return(trigger, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(trigger)
|
||||
}))
|
||||
s.Run("GetAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
trigger := testutil.Fake(s.T(), faker, database.AutomationTrigger{AutomationID: automation.ID})
|
||||
dbm.EXPECT().GetAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
check.Args(trigger.ID).Asserts(automation, policy.ActionRead).Returns(trigger)
|
||||
}))
|
||||
s.Run("GetAutomationTriggersByAutomationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationTriggersByAutomationID(gomock.Any(), automation.ID).Return([]database.AutomationTrigger{}, nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns([]database.AutomationTrigger{})
|
||||
}))
|
||||
s.Run("UpdateAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
trigger := testutil.Fake(s.T(), faker, database.AutomationTrigger{AutomationID: automation.ID})
|
||||
arg := database.UpdateAutomationTriggerParams{ID: trigger.ID}
|
||||
dbm.EXPECT().GetAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateAutomationTrigger(gomock.Any(), arg).Return(trigger, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(trigger)
|
||||
}))
|
||||
s.Run("UpdateAutomationTriggerWebhookSecret", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
trigger := testutil.Fake(s.T(), faker, database.AutomationTrigger{AutomationID: automation.ID})
|
||||
arg := database.UpdateAutomationTriggerWebhookSecretParams{ID: trigger.ID, WebhookSecret: sql.NullString{String: "new-secret", Valid: true}}
|
||||
dbm.EXPECT().GetAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateAutomationTriggerWebhookSecret(gomock.Any(), arg).Return(trigger, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(trigger)
|
||||
}))
|
||||
s.Run("DeleteAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.Automation{})
|
||||
trigger := testutil.Fake(s.T(), faker, database.AutomationTrigger{AutomationID: automation.ID})
|
||||
dbm.EXPECT().GetAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteAutomationTriggerByID(gomock.Any(), trigger.ID).Return(nil).AnyTimes()
|
||||
check.Args(trigger.ID).Asserts(automation, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("GetAuthorizedAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetAutomationsParams{}
|
||||
dbm.EXPECT().GetAuthorizedAutomations(gomock.Any(), params, gomock.Any()).Return([]database.Automation{}, nil).AnyTimes()
|
||||
// No asserts here because it re-routes through GetAutomations which uses SQLFilter.
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
}
|
||||
|
||||
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{})
|
||||
|
||||
@@ -103,7 +103,6 @@ func (m queryMetricsStore) DeleteOrganization(ctx context.Context, id uuid.UUID)
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteOrganization").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) AcquireChats(ctx context.Context, arg database.AcquireChatsParams) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.AcquireChats(ctx, arg)
|
||||
@@ -296,6 +295,22 @@ func (m queryMetricsStore) CountAuditLogs(ctx context.Context, arg database.Coun
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountAutomationChatCreatesInWindow(ctx context.Context, arg database.CountAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountAutomationChatCreatesInWindow(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("CountAutomationChatCreatesInWindow").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountAutomationChatCreatesInWindow").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountAutomationMessagesInWindow(ctx context.Context, arg database.CountAutomationMessagesInWindowParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountAutomationMessagesInWindow(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("CountAutomationMessagesInWindow").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountAutomationMessagesInWindow").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 +415,22 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAutomationByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAutomationByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAutomationByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAutomationTriggerByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAutomationTriggerByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAutomationTriggerByID").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 +967,14 @@ func (m queryMetricsStore) GetActiveAISeatCount(ctx context.Context) (int64, err
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetActiveCronTriggers(ctx context.Context) ([]database.GetActiveCronTriggersRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetActiveCronTriggers(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetActiveCronTriggers").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetActiveCronTriggers").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 +1071,46 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAutomationByID(ctx context.Context, id uuid.UUID) (database.Automation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAutomationByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAutomationByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutomationByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAutomationEvents(ctx context.Context, arg database.GetAutomationEventsParams) ([]database.AutomationEvent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAutomationEvents(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetAutomationEvents").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutomationEvents").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.AutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAutomationTriggerByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAutomationTriggerByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutomationTriggerByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.AutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAutomationTriggersByAutomationID(ctx, automationID)
|
||||
m.queryLatencies.WithLabelValues("GetAutomationTriggersByAutomationID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutomationTriggersByAutomationID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAutomations(ctx context.Context, arg database.GetAutomationsParams) ([]database.Automation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAutomations(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetAutomations").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAutomations").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)
|
||||
@@ -3176,6 +3255,30 @@ func (m queryMetricsStore) InsertAuditLog(ctx context.Context, arg database.Inse
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAutomation(ctx context.Context, arg database.InsertAutomationParams) (database.Automation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAutomation(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAutomation").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAutomation").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAutomationEvent(ctx context.Context, arg database.InsertAutomationEventParams) (database.AutomationEvent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAutomationEvent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAutomationEvent").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAutomationEvent").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAutomationTrigger(ctx context.Context, arg database.InsertAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAutomationTrigger(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAutomationTrigger").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAutomationTrigger").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertChat(ctx, arg)
|
||||
@@ -3880,6 +3983,14 @@ func (m queryMetricsStore) PopNextQueuedMessage(ctx context.Context, chatID uuid
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) PurgeOldAutomationEvents(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.PurgeOldAutomationEvents(ctx)
|
||||
m.queryLatencies.WithLabelValues("PurgeOldAutomationEvents").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "PurgeOldAutomationEvents").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID)
|
||||
@@ -4000,6 +4111,38 @@ func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.Up
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAutomation(ctx context.Context, arg database.UpdateAutomationParams) (database.Automation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateAutomation(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAutomation").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAutomation").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAutomationTrigger(ctx context.Context, arg database.UpdateAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateAutomationTrigger(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAutomationTrigger").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAutomationTrigger").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateAutomationTriggerLastTriggeredAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateAutomationTriggerLastTriggeredAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAutomationTriggerLastTriggeredAt").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAutomationTriggerLastTriggeredAt").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateAutomationTriggerWebhookSecretParams) (database.AutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateAutomationTriggerWebhookSecret(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAutomationTriggerWebhookSecret").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAutomationTriggerWebhookSecret").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatByID(ctx, arg)
|
||||
@@ -5199,3 +5342,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) GetAuthorizedAutomations(ctx context.Context, arg database.GetAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.Automation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedAutomations(ctx, arg, prepared)
|
||||
m.queryLatencies.WithLabelValues("GetAuthorizedAutomations").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedAutomations").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
@@ -453,6 +453,36 @@ func (mr *MockStoreMockRecorder) CountAuthorizedConnectionLogs(ctx, arg, prepare
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAuthorizedConnectionLogs", reflect.TypeOf((*MockStore)(nil).CountAuthorizedConnectionLogs), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// CountAutomationChatCreatesInWindow mocks base method.
|
||||
func (m *MockStore) CountAutomationChatCreatesInWindow(ctx context.Context, arg database.CountAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CountAutomationChatCreatesInWindow", ctx, arg)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CountAutomationChatCreatesInWindow indicates an expected call of CountAutomationChatCreatesInWindow.
|
||||
func (mr *MockStoreMockRecorder) CountAutomationChatCreatesInWindow(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAutomationChatCreatesInWindow", reflect.TypeOf((*MockStore)(nil).CountAutomationChatCreatesInWindow), ctx, arg)
|
||||
}
|
||||
|
||||
// CountAutomationMessagesInWindow mocks base method.
|
||||
func (m *MockStore) CountAutomationMessagesInWindow(ctx context.Context, arg database.CountAutomationMessagesInWindowParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CountAutomationMessagesInWindow", ctx, arg)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CountAutomationMessagesInWindow indicates an expected call of CountAutomationMessagesInWindow.
|
||||
func (mr *MockStoreMockRecorder) CountAutomationMessagesInWindow(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAutomationMessagesInWindow", reflect.TypeOf((*MockStore)(nil).CountAutomationMessagesInWindow), ctx, arg)
|
||||
}
|
||||
|
||||
// CountConnectionLogs mocks base method.
|
||||
func (m *MockStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -642,6 +672,34 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID)
|
||||
}
|
||||
|
||||
// DeleteAutomationByID mocks base method.
|
||||
func (m *MockStore) DeleteAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAutomationByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAutomationByID indicates an expected call of DeleteAutomationByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteAutomationByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAutomationByID", reflect.TypeOf((*MockStore)(nil).DeleteAutomationByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteAutomationTriggerByID mocks base method.
|
||||
func (m *MockStore) DeleteAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAutomationTriggerByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAutomationTriggerByID indicates an expected call of DeleteAutomationTriggerByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteAutomationTriggerByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).DeleteAutomationTriggerByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteChatModelConfigByID mocks base method.
|
||||
func (m *MockStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1608,6 +1666,21 @@ func (mr *MockStoreMockRecorder) GetActiveAISeatCount(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveAISeatCount", reflect.TypeOf((*MockStore)(nil).GetActiveAISeatCount), ctx)
|
||||
}
|
||||
|
||||
// GetActiveCronTriggers mocks base method.
|
||||
func (m *MockStore) GetActiveCronTriggers(ctx context.Context) ([]database.GetActiveCronTriggersRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveCronTriggers", ctx)
|
||||
ret0, _ := ret[0].([]database.GetActiveCronTriggersRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveCronTriggers indicates an expected call of GetActiveCronTriggers.
|
||||
func (mr *MockStoreMockRecorder) GetActiveCronTriggers(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveCronTriggers", reflect.TypeOf((*MockStore)(nil).GetActiveCronTriggers), ctx)
|
||||
}
|
||||
|
||||
// GetActivePresetPrebuildSchedules mocks base method.
|
||||
func (m *MockStore) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1803,6 +1876,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedAuditLogsOffset(ctx, arg, prepared
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedAuditLogsOffset", reflect.TypeOf((*MockStore)(nil).GetAuthorizedAuditLogsOffset), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedAutomations mocks base method.
|
||||
func (m *MockStore) GetAuthorizedAutomations(ctx context.Context, arg database.GetAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.Automation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthorizedAutomations", ctx, arg, prepared)
|
||||
ret0, _ := ret[0].([]database.Automation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAuthorizedAutomations indicates an expected call of GetAuthorizedAutomations.
|
||||
func (mr *MockStoreMockRecorder) GetAuthorizedAutomations(ctx, arg, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedAutomations", reflect.TypeOf((*MockStore)(nil).GetAuthorizedAutomations), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedChats mocks base method.
|
||||
func (m *MockStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1893,6 +1981,81 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx,
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared)
|
||||
}
|
||||
|
||||
// GetAutomationByID mocks base method.
|
||||
func (m *MockStore) GetAutomationByID(ctx context.Context, id uuid.UUID) (database.Automation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAutomationByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.Automation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAutomationByID indicates an expected call of GetAutomationByID.
|
||||
func (mr *MockStoreMockRecorder) GetAutomationByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomationByID", reflect.TypeOf((*MockStore)(nil).GetAutomationByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAutomationEvents mocks base method.
|
||||
func (m *MockStore) GetAutomationEvents(ctx context.Context, arg database.GetAutomationEventsParams) ([]database.AutomationEvent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAutomationEvents", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.AutomationEvent)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAutomationEvents indicates an expected call of GetAutomationEvents.
|
||||
func (mr *MockStoreMockRecorder) GetAutomationEvents(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomationEvents", reflect.TypeOf((*MockStore)(nil).GetAutomationEvents), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAutomationTriggerByID mocks base method.
|
||||
func (m *MockStore) GetAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.AutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAutomationTriggerByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.AutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAutomationTriggerByID indicates an expected call of GetAutomationTriggerByID.
|
||||
func (mr *MockStoreMockRecorder) GetAutomationTriggerByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).GetAutomationTriggerByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAutomationTriggersByAutomationID mocks base method.
|
||||
func (m *MockStore) GetAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.AutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAutomationTriggersByAutomationID", ctx, automationID)
|
||||
ret0, _ := ret[0].([]database.AutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAutomationTriggersByAutomationID indicates an expected call of GetAutomationTriggersByAutomationID.
|
||||
func (mr *MockStoreMockRecorder) GetAutomationTriggersByAutomationID(ctx, automationID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomationTriggersByAutomationID", reflect.TypeOf((*MockStore)(nil).GetAutomationTriggersByAutomationID), ctx, automationID)
|
||||
}
|
||||
|
||||
// GetAutomations mocks base method.
|
||||
func (m *MockStore) GetAutomations(ctx context.Context, arg database.GetAutomationsParams) ([]database.Automation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAutomations", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.Automation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAutomations indicates an expected call of GetAutomations.
|
||||
func (mr *MockStoreMockRecorder) GetAutomations(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomations", reflect.TypeOf((*MockStore)(nil).GetAutomations), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatByID mocks base method.
|
||||
func (m *MockStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -5957,6 +6120,51 @@ func (mr *MockStoreMockRecorder) InsertAuditLog(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAuditLog", reflect.TypeOf((*MockStore)(nil).InsertAuditLog), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAutomation mocks base method.
|
||||
func (m *MockStore) InsertAutomation(ctx context.Context, arg database.InsertAutomationParams) (database.Automation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAutomation", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Automation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAutomation indicates an expected call of InsertAutomation.
|
||||
func (mr *MockStoreMockRecorder) InsertAutomation(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAutomation", reflect.TypeOf((*MockStore)(nil).InsertAutomation), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAutomationEvent mocks base method.
|
||||
func (m *MockStore) InsertAutomationEvent(ctx context.Context, arg database.InsertAutomationEventParams) (database.AutomationEvent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAutomationEvent", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AutomationEvent)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAutomationEvent indicates an expected call of InsertAutomationEvent.
|
||||
func (mr *MockStoreMockRecorder) InsertAutomationEvent(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAutomationEvent", reflect.TypeOf((*MockStore)(nil).InsertAutomationEvent), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAutomationTrigger mocks base method.
|
||||
func (m *MockStore) InsertAutomationTrigger(ctx context.Context, arg database.InsertAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAutomationTrigger", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAutomationTrigger indicates an expected call of InsertAutomationTrigger.
|
||||
func (mr *MockStoreMockRecorder) InsertAutomationTrigger(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAutomationTrigger", reflect.TypeOf((*MockStore)(nil).InsertAutomationTrigger), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertChat mocks base method.
|
||||
func (m *MockStore) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7336,6 +7544,20 @@ func (mr *MockStoreMockRecorder) PopNextQueuedMessage(ctx, chatID any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopNextQueuedMessage", reflect.TypeOf((*MockStore)(nil).PopNextQueuedMessage), ctx, chatID)
|
||||
}
|
||||
|
||||
// PurgeOldAutomationEvents mocks base method.
|
||||
func (m *MockStore) PurgeOldAutomationEvents(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PurgeOldAutomationEvents", ctx)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PurgeOldAutomationEvents indicates an expected call of PurgeOldAutomationEvents.
|
||||
func (mr *MockStoreMockRecorder) PurgeOldAutomationEvents(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgeOldAutomationEvents", reflect.TypeOf((*MockStore)(nil).PurgeOldAutomationEvents), ctx)
|
||||
}
|
||||
|
||||
// ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate mocks base method.
|
||||
func (m *MockStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7552,6 +7774,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)
|
||||
}
|
||||
|
||||
// UpdateAutomation mocks base method.
|
||||
func (m *MockStore) UpdateAutomation(ctx context.Context, arg database.UpdateAutomationParams) (database.Automation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAutomation", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Automation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateAutomation indicates an expected call of UpdateAutomation.
|
||||
func (mr *MockStoreMockRecorder) UpdateAutomation(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAutomation", reflect.TypeOf((*MockStore)(nil).UpdateAutomation), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAutomationTrigger mocks base method.
|
||||
func (m *MockStore) UpdateAutomationTrigger(ctx context.Context, arg database.UpdateAutomationTriggerParams) (database.AutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAutomationTrigger", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateAutomationTrigger indicates an expected call of UpdateAutomationTrigger.
|
||||
func (mr *MockStoreMockRecorder) UpdateAutomationTrigger(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAutomationTrigger", reflect.TypeOf((*MockStore)(nil).UpdateAutomationTrigger), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAutomationTriggerLastTriggeredAt mocks base method.
|
||||
func (m *MockStore) UpdateAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateAutomationTriggerLastTriggeredAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAutomationTriggerLastTriggeredAt", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAutomationTriggerLastTriggeredAt indicates an expected call of UpdateAutomationTriggerLastTriggeredAt.
|
||||
func (mr *MockStoreMockRecorder) UpdateAutomationTriggerLastTriggeredAt(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAutomationTriggerLastTriggeredAt", reflect.TypeOf((*MockStore)(nil).UpdateAutomationTriggerLastTriggeredAt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAutomationTriggerWebhookSecret mocks base method.
|
||||
func (m *MockStore) UpdateAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateAutomationTriggerWebhookSecretParams) (database.AutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAutomationTriggerWebhookSecret", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateAutomationTriggerWebhookSecret indicates an expected call of UpdateAutomationTriggerWebhookSecret.
|
||||
func (mr *MockStoreMockRecorder) UpdateAutomationTriggerWebhookSecret(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAutomationTriggerWebhookSecret", reflect.TypeOf((*MockStore)(nil).UpdateAutomationTriggerWebhookSecret), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatByID mocks base method.
|
||||
func (m *MockStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+106
-1
@@ -1212,6 +1212,66 @@ CREATE TABLE audit_logs (
|
||||
resource_icon text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE automation_events (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
trigger_id uuid,
|
||||
received_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
filter_matched boolean NOT NULL,
|
||||
resolved_labels jsonb,
|
||||
matched_chat_id uuid,
|
||||
created_chat_id uuid,
|
||||
status text NOT NULL,
|
||||
error text,
|
||||
CONSTRAINT automation_events_status_check CHECK ((status = ANY (ARRAY['filtered'::text, 'preview'::text, 'created'::text, 'continued'::text, 'rate_limited'::text, 'error'::text])))
|
||||
);
|
||||
|
||||
CREATE TABLE automation_triggers (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
type text NOT NULL,
|
||||
webhook_secret text,
|
||||
webhook_secret_key_id text,
|
||||
cron_schedule text,
|
||||
filter jsonb,
|
||||
label_paths jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
last_triggered_at timestamp with time zone,
|
||||
CONSTRAINT automation_triggers_type_check CHECK ((type = ANY (ARRAY['webhook'::text, 'cron'::text])))
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.webhook_secret_key_id IS 'The ID of the key used to encrypt the webhook secret. If NULL, the secret is not encrypted.';
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.filter IS 'gjson filter conditions for webhook triggers. NULL means match everything.';
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.label_paths IS 'Map of chat label keys to gjson paths for extracting values from webhook payloads.';
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.last_triggered_at IS 'The last time this cron trigger was evaluated and fired. Used by the cron scheduler to determine which triggers are due.';
|
||||
|
||||
CREATE TABLE automations (
|
||||
id uuid DEFAULT gen_random_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 text DEFAULT 'disabled'::text 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 DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT automations_max_chat_creates_per_hour_check CHECK ((max_chat_creates_per_hour > 0)),
|
||||
CONSTRAINT automations_max_messages_per_hour_check CHECK ((max_messages_per_hour > 0)),
|
||||
CONSTRAINT automations_status_check CHECK ((status = ANY (ARRAY['disabled'::text, 'preview'::text, 'active'::text])))
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN automations.instructions IS 'User message sent to the chat when the automation triggers.';
|
||||
|
||||
CREATE TABLE boundary_usage_stats (
|
||||
replica_id uuid NOT NULL,
|
||||
unique_workspaces_count bigint DEFAULT 0 NOT NULL,
|
||||
@@ -1399,7 +1459,8 @@ CREATE TABLE chats (
|
||||
last_error text,
|
||||
mode chat_mode,
|
||||
mcp_server_ids uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||
labels jsonb DEFAULT '{}'::jsonb NOT NULL
|
||||
labels jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
automation_id uuid
|
||||
);
|
||||
|
||||
CREATE TABLE connection_logs (
|
||||
@@ -3311,6 +3372,15 @@ ALTER TABLE ONLY api_keys
|
||||
ALTER TABLE ONLY audit_logs
|
||||
ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY automation_events
|
||||
ADD CONSTRAINT automation_events_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY automation_triggers
|
||||
ADD CONSTRAINT automation_triggers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY automations
|
||||
ADD CONSTRAINT automations_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY boundary_usage_stats
|
||||
ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
|
||||
|
||||
@@ -3699,6 +3769,18 @@ 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_automation_events_automation_id_received_at ON automation_events USING btree (automation_id, received_at DESC);
|
||||
|
||||
CREATE INDEX idx_automation_events_received_at ON automation_events USING btree (received_at);
|
||||
|
||||
CREATE INDEX idx_automation_triggers_automation_id ON automation_triggers USING btree (automation_id);
|
||||
|
||||
CREATE INDEX idx_automations_organization_id ON automations USING btree (organization_id);
|
||||
|
||||
CREATE INDEX idx_automations_owner_id ON automations USING btree (owner_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_automations_owner_org_name ON 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);
|
||||
@@ -3727,6 +3809,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);
|
||||
@@ -4000,6 +4084,24 @@ 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 automation_events
|
||||
ADD CONSTRAINT automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY automation_events
|
||||
ADD CONSTRAINT automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES automation_triggers(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY automation_triggers
|
||||
ADD CONSTRAINT automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY automations
|
||||
ADD CONSTRAINT automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY automations
|
||||
ADD CONSTRAINT automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY automations
|
||||
ADD CONSTRAINT 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;
|
||||
|
||||
@@ -4033,6 +4135,9 @@ ALTER TABLE ONLY chat_providers
|
||||
ALTER TABLE ONLY chat_queued_messages
|
||||
ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ 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;
|
||||
ForeignKeyAutomationEventsAutomationID ForeignKeyConstraint = "automation_events_automation_id_fkey" // ALTER TABLE ONLY automation_events ADD CONSTRAINT automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE;
|
||||
ForeignKeyAutomationEventsTriggerID ForeignKeyConstraint = "automation_events_trigger_id_fkey" // ALTER TABLE ONLY automation_events ADD CONSTRAINT automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES automation_triggers(id) ON DELETE SET NULL;
|
||||
ForeignKeyAutomationTriggersAutomationID ForeignKeyConstraint = "automation_triggers_automation_id_fkey" // ALTER TABLE ONLY automation_triggers ADD CONSTRAINT automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE;
|
||||
ForeignKeyAutomationsModelConfigID ForeignKeyConstraint = "automations_model_config_id_fkey" // ALTER TABLE ONLY automations ADD CONSTRAINT automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
|
||||
ForeignKeyAutomationsOrganizationID ForeignKeyConstraint = "automations_organization_id_fkey" // ALTER TABLE ONLY automations ADD CONSTRAINT automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyAutomationsOwnerID ForeignKeyConstraint = "automations_owner_id_fkey" // ALTER TABLE ONLY automations ADD CONSTRAINT 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;
|
||||
@@ -20,6 +26,7 @@ const (
|
||||
ForeignKeyChatProvidersAPIKeyKeyID ForeignKeyConstraint = "chat_providers_api_key_key_id_fkey" // ALTER TABLE ONLY chat_providers ADD CONSTRAINT chat_providers_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
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;
|
||||
ForeignKeyChatsAutomationID ForeignKeyConstraint = "chats_automation_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES automations(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;
|
||||
ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestCustomQueriesSyncedRowScan(t *testing.T) {
|
||||
"GetWorkspaces": "GetAuthorizedWorkspaces",
|
||||
"GetUsers": "GetAuthorizedUsers",
|
||||
"GetChats": "GetAuthorizedChats",
|
||||
"GetAutomations": "GetAuthorizedAutomations",
|
||||
}
|
||||
|
||||
// Scan custom
|
||||
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
LockIDReconcilePrebuilds
|
||||
LockIDReconcileSystemRoles
|
||||
LockIDBoundaryUsageStats
|
||||
LockIDAutomationCron
|
||||
)
|
||||
|
||||
// GenLockID generates a unique and consistent lock ID from a given string.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE chats DROP COLUMN IF EXISTS automation_id;
|
||||
|
||||
DROP TABLE IF EXISTS automation_events;
|
||||
|
||||
DROP TABLE IF EXISTS automation_triggers;
|
||||
|
||||
DROP TABLE IF EXISTS automations;
|
||||
@@ -0,0 +1,78 @@
|
||||
CREATE TABLE automations (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
instructions text NOT NULL DEFAULT '',
|
||||
model_config_id uuid,
|
||||
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
allowed_tools text[] NOT NULL DEFAULT '{}',
|
||||
status text NOT NULL DEFAULT 'disabled',
|
||||
max_chat_creates_per_hour integer NOT NULL DEFAULT 10,
|
||||
max_messages_per_hour integer NOT NULL DEFAULT 60,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
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 automations_status_check CHECK (status IN ('disabled', 'preview', 'active')),
|
||||
CONSTRAINT automations_max_chat_creates_per_hour_check CHECK (max_chat_creates_per_hour > 0),
|
||||
CONSTRAINT automations_max_messages_per_hour_check CHECK (max_messages_per_hour > 0)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN automations.instructions IS 'User message sent to the chat when the automation triggers.';
|
||||
|
||||
CREATE INDEX idx_automations_owner_id ON automations (owner_id);
|
||||
CREATE INDEX idx_automations_organization_id ON automations (organization_id);
|
||||
|
||||
CREATE TABLE automation_triggers (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
type text NOT NULL,
|
||||
webhook_secret text,
|
||||
webhook_secret_key_id text,
|
||||
cron_schedule text,
|
||||
filter jsonb,
|
||||
label_paths jsonb,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT automation_triggers_type_check CHECK (type IN ('webhook', 'cron'))
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.webhook_secret_key_id IS 'The ID of the key used to encrypt the webhook secret. If NULL, the secret is not encrypted.';
|
||||
COMMENT ON COLUMN automation_triggers.filter IS 'gjson filter conditions for webhook triggers. NULL means match everything.';
|
||||
COMMENT ON COLUMN automation_triggers.label_paths IS 'Map of chat label keys to gjson paths for extracting values from webhook payloads.';
|
||||
|
||||
CREATE INDEX idx_automation_triggers_automation_id ON automation_triggers (automation_id);
|
||||
|
||||
CREATE TABLE automation_events (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
trigger_id uuid,
|
||||
received_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
filter_matched boolean NOT NULL,
|
||||
resolved_labels jsonb,
|
||||
matched_chat_id uuid,
|
||||
created_chat_id uuid,
|
||||
status text NOT NULL,
|
||||
error text,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (automation_id) REFERENCES automations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (trigger_id) REFERENCES automation_triggers(id) ON DELETE SET NULL,
|
||||
CONSTRAINT automation_events_status_check CHECK (status IN ('filtered', 'preview', 'created', 'continued', 'rate_limited', 'error'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_automation_events_automation_id_received_at ON automation_events (automation_id, received_at DESC);
|
||||
|
||||
ALTER TABLE chats ADD COLUMN automation_id uuid REFERENCES automations(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX idx_chats_automation_id ON chats (automation_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_automations_owner_org_name ON automations (owner_id, organization_id, name);
|
||||
|
||||
CREATE INDEX idx_automation_events_received_at ON automation_events (received_at);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE automation_triggers DROP COLUMN last_triggered_at;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE automation_triggers ADD COLUMN last_triggered_at timestamp with time zone;
|
||||
|
||||
COMMENT ON COLUMN automation_triggers.last_triggered_at IS 'The last time this cron trigger was evaluated and fired. Used by the cron scheduler to determine which triggers are due.';
|
||||
@@ -178,6 +178,13 @@ func (c Chat) RBACObject() rbac.Object {
|
||||
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String())
|
||||
}
|
||||
|
||||
func (a Automation) RBACObject() rbac.Object {
|
||||
return rbac.ResourceAutomation.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ type customQuerier interface {
|
||||
connectionLogQuerier
|
||||
aibridgeQuerier
|
||||
chatQuerier
|
||||
automationQuerier
|
||||
}
|
||||
|
||||
type templateQuerier interface {
|
||||
@@ -791,6 +792,68 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
); 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 automationQuerier interface {
|
||||
GetAuthorizedAutomations(ctx context.Context, arg GetAutomationsParams, prepared rbac.PreparedAuthorized) ([]Automation, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedAutomations(ctx context.Context, arg GetAutomationsParams, prepared rbac.PreparedAuthorized) ([]Automation, 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(getAutomations, 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: GetAuthorizedAutomations :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 []Automation
|
||||
for rows.Next() {
|
||||
var i Automation
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4134,6 +4134,56 @@ type AuditLog struct {
|
||||
ResourceIcon string `db:"resource_icon" json:"resource_icon"`
|
||||
}
|
||||
|
||||
type Automation struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
// User message sent to the chat when the automation triggers.
|
||||
Instructions string `db:"instructions" json:"instructions"`
|
||||
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
AllowedTools []string `db:"allowed_tools" json:"allowed_tools"`
|
||||
Status string `db:"status" json:"status"`
|
||||
MaxChatCreatesPerHour int32 `db:"max_chat_creates_per_hour" json:"max_chat_creates_per_hour"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type AutomationEvent 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"`
|
||||
Payload json.RawMessage `db:"payload" json:"payload"`
|
||||
FilterMatched bool `db:"filter_matched" json:"filter_matched"`
|
||||
ResolvedLabels pqtype.NullRawMessage `db:"resolved_labels" json:"resolved_labels"`
|
||||
MatchedChatID uuid.NullUUID `db:"matched_chat_id" json:"matched_chat_id"`
|
||||
CreatedChatID uuid.NullUUID `db:"created_chat_id" json:"created_chat_id"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Error sql.NullString `db:"error" json:"error"`
|
||||
}
|
||||
|
||||
type AutomationTrigger struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
Type string `db:"type" json:"type"`
|
||||
WebhookSecret sql.NullString `db:"webhook_secret" json:"webhook_secret"`
|
||||
// The ID of the key used to encrypt the webhook secret. If NULL, the secret is not encrypted.
|
||||
WebhookSecretKeyID sql.NullString `db:"webhook_secret_key_id" json:"webhook_secret_key_id"`
|
||||
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
|
||||
// gjson filter conditions for webhook triggers. NULL means match everything.
|
||||
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
|
||||
// Map of chat label keys to gjson paths for extracting values from webhook payloads.
|
||||
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"`
|
||||
// The last time this cron trigger was evaluated and fired. Used by the cron scheduler to determine which triggers are due.
|
||||
LastTriggeredAt sql.NullTime `db:"last_triggered_at" json:"last_triggered_at"`
|
||||
}
|
||||
|
||||
// Per-replica boundary usage statistics for telemetry aggregation.
|
||||
type BoundaryUsageStat struct {
|
||||
// The unique identifier of the replica reporting stats.
|
||||
@@ -4171,6 +4221,7 @@ type Chat struct {
|
||||
Mode NullChatMode `db:"mode" json:"mode"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
Labels StringMap `db:"labels" json:"labels"`
|
||||
AutomationID uuid.NullUUID `db:"automation_id" json:"automation_id"`
|
||||
}
|
||||
|
||||
type ChatDiffStatus struct {
|
||||
|
||||
@@ -78,6 +78,8 @@ type sqlcQuerier interface {
|
||||
CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error)
|
||||
CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error)
|
||||
CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error)
|
||||
CountAutomationChatCreatesInWindow(ctx context.Context, arg CountAutomationChatCreatesInWindowParams) (int64, error)
|
||||
CountAutomationMessagesInWindow(ctx context.Context, arg CountAutomationMessagesInWindowParams) (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 +102,8 @@ type sqlcQuerier interface {
|
||||
// be recreated.
|
||||
DeleteAllWebpushSubscriptions(ctx context.Context) error
|
||||
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteAutomationByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteAutomationTriggerByID(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 +201,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.
|
||||
GetActiveCronTriggers(ctx context.Context) ([]GetActiveCronTriggersRow, 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 +231,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)
|
||||
GetAutomationByID(ctx context.Context, id uuid.UUID) (Automation, error)
|
||||
GetAutomationEvents(ctx context.Context, arg GetAutomationEventsParams) ([]AutomationEvent, error)
|
||||
GetAutomationTriggerByID(ctx context.Context, id uuid.UUID) (AutomationTrigger, error)
|
||||
GetAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]AutomationTrigger, error)
|
||||
GetAutomations(ctx context.Context, arg GetAutomationsParams) ([]Automation, 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.
|
||||
@@ -678,6 +691,9 @@ type sqlcQuerier interface {
|
||||
// every member of the org.
|
||||
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
|
||||
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
|
||||
InsertAutomation(ctx context.Context, arg InsertAutomationParams) (Automation, error)
|
||||
InsertAutomationEvent(ctx context.Context, arg InsertAutomationEventParams) (AutomationEvent, error)
|
||||
InsertAutomationTrigger(ctx context.Context, arg InsertAutomationTriggerParams) (AutomationTrigger, error)
|
||||
InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error)
|
||||
InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error)
|
||||
InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error)
|
||||
@@ -790,6 +806,7 @@ type sqlcQuerier interface {
|
||||
OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error)
|
||||
PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error)
|
||||
PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error)
|
||||
PurgeOldAutomationEvents(ctx context.Context) 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)
|
||||
@@ -819,6 +836,10 @@ type sqlcQuerier interface {
|
||||
UnsetDefaultChatModelConfigs(ctx context.Context) error
|
||||
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateAutomation(ctx context.Context, arg UpdateAutomationParams) (Automation, error)
|
||||
UpdateAutomationTrigger(ctx context.Context, arg UpdateAutomationTriggerParams) (AutomationTrigger, error)
|
||||
UpdateAutomationTriggerLastTriggeredAt(ctx context.Context, arg UpdateAutomationTriggerLastTriggeredAtParams) error
|
||||
UpdateAutomationTriggerWebhookSecret(ctx context.Context, arg UpdateAutomationTriggerWebhookSecretParams) (AutomationTrigger, error)
|
||||
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
|
||||
// Bumps the heartbeat timestamp for a running chat so that other
|
||||
// replicas know the worker is still alive.
|
||||
|
||||
+765
-11
@@ -2517,6 +2517,749 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam
|
||||
return i, err
|
||||
}
|
||||
|
||||
const countAutomationChatCreatesInWindow = `-- name: CountAutomationChatCreatesInWindow :one
|
||||
SELECT COUNT(*)
|
||||
FROM automation_events
|
||||
WHERE automation_id = $1::uuid
|
||||
AND status = 'created'
|
||||
AND received_at > $2::timestamptz
|
||||
`
|
||||
|
||||
type CountAutomationChatCreatesInWindowParams struct {
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
WindowStart time.Time `db:"window_start" json:"window_start"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) CountAutomationChatCreatesInWindow(ctx context.Context, arg CountAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countAutomationChatCreatesInWindow, arg.AutomationID, arg.WindowStart)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countAutomationMessagesInWindow = `-- name: CountAutomationMessagesInWindow :one
|
||||
SELECT COUNT(*)
|
||||
FROM automation_events
|
||||
WHERE automation_id = $1::uuid
|
||||
AND status IN ('created', 'continued')
|
||||
AND received_at > $2::timestamptz
|
||||
`
|
||||
|
||||
type CountAutomationMessagesInWindowParams struct {
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
WindowStart time.Time `db:"window_start" json:"window_start"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) CountAutomationMessagesInWindow(ctx context.Context, arg CountAutomationMessagesInWindowParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countAutomationMessagesInWindow, arg.AutomationID, arg.WindowStart)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getAutomationEvents = `-- name: GetAutomationEvents :many
|
||||
SELECT
|
||||
id, automation_id, trigger_id, received_at, payload, filter_matched, resolved_labels, matched_chat_id, created_chat_id, status, error
|
||||
FROM
|
||||
automation_events
|
||||
WHERE
|
||||
automation_id = $1::uuid
|
||||
AND CASE
|
||||
WHEN $2::text IS NOT NULL THEN status = $2::text
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY
|
||||
received_at DESC
|
||||
OFFSET $3
|
||||
LIMIT
|
||||
COALESCE(NULLIF($4 :: int, 0), 50)
|
||||
`
|
||||
|
||||
type GetAutomationEventsParams struct {
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
StatusFilter sql.NullString `db:"status_filter" json:"status_filter"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAutomationEvents(ctx context.Context, arg GetAutomationEventsParams) ([]AutomationEvent, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAutomationEvents,
|
||||
arg.AutomationID,
|
||||
arg.StatusFilter,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AutomationEvent
|
||||
for rows.Next() {
|
||||
var i AutomationEvent
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.TriggerID,
|
||||
&i.ReceivedAt,
|
||||
&i.Payload,
|
||||
&i.FilterMatched,
|
||||
&i.ResolvedLabels,
|
||||
&i.MatchedChatID,
|
||||
&i.CreatedChatID,
|
||||
&i.Status,
|
||||
&i.Error,
|
||||
); 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
|
||||
}
|
||||
|
||||
const insertAutomationEvent = `-- name: InsertAutomationEvent :one
|
||||
INSERT INTO automation_events (
|
||||
automation_id,
|
||||
trigger_id,
|
||||
payload,
|
||||
filter_matched,
|
||||
resolved_labels,
|
||||
matched_chat_id,
|
||||
created_chat_id,
|
||||
status,
|
||||
error
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
$3::jsonb,
|
||||
$4::boolean,
|
||||
$5::jsonb,
|
||||
$6::uuid,
|
||||
$7::uuid,
|
||||
$8::text,
|
||||
$9::text
|
||||
) RETURNING id, automation_id, trigger_id, received_at, payload, filter_matched, resolved_labels, matched_chat_id, created_chat_id, status, error
|
||||
`
|
||||
|
||||
type InsertAutomationEventParams struct {
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
TriggerID uuid.NullUUID `db:"trigger_id" json:"trigger_id"`
|
||||
Payload json.RawMessage `db:"payload" json:"payload"`
|
||||
FilterMatched bool `db:"filter_matched" json:"filter_matched"`
|
||||
ResolvedLabels pqtype.NullRawMessage `db:"resolved_labels" json:"resolved_labels"`
|
||||
MatchedChatID uuid.NullUUID `db:"matched_chat_id" json:"matched_chat_id"`
|
||||
CreatedChatID uuid.NullUUID `db:"created_chat_id" json:"created_chat_id"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Error sql.NullString `db:"error" json:"error"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAutomationEvent(ctx context.Context, arg InsertAutomationEventParams) (AutomationEvent, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAutomationEvent,
|
||||
arg.AutomationID,
|
||||
arg.TriggerID,
|
||||
arg.Payload,
|
||||
arg.FilterMatched,
|
||||
arg.ResolvedLabels,
|
||||
arg.MatchedChatID,
|
||||
arg.CreatedChatID,
|
||||
arg.Status,
|
||||
arg.Error,
|
||||
)
|
||||
var i AutomationEvent
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.TriggerID,
|
||||
&i.ReceivedAt,
|
||||
&i.Payload,
|
||||
&i.FilterMatched,
|
||||
&i.ResolvedLabels,
|
||||
&i.MatchedChatID,
|
||||
&i.CreatedChatID,
|
||||
&i.Status,
|
||||
&i.Error,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const purgeOldAutomationEvents = `-- name: PurgeOldAutomationEvents :exec
|
||||
DELETE FROM automation_events
|
||||
WHERE received_at < NOW() - INTERVAL '7 days'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) PurgeOldAutomationEvents(ctx context.Context) error {
|
||||
_, err := q.db.ExecContext(ctx, purgeOldAutomationEvents)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteAutomationByID = `-- name: DeleteAutomationByID :exec
|
||||
DELETE FROM automations WHERE id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAutomationByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAutomationByID = `-- name: GetAutomationByID :one
|
||||
SELECT 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 FROM automations WHERE id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAutomationByID(ctx context.Context, id uuid.UUID) (Automation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAutomationByID, id)
|
||||
var i Automation
|
||||
err := row.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,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAutomations = `-- name: GetAutomations :many
|
||||
SELECT
|
||||
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
|
||||
FROM
|
||||
automations
|
||||
WHERE
|
||||
CASE
|
||||
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN automations.owner_id = $1
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN automations.organization_id = $2
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedAutomations
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
OFFSET $3
|
||||
LIMIT
|
||||
COALESCE(NULLIF($4 :: int, 0), 50)
|
||||
`
|
||||
|
||||
type GetAutomationsParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAutomations(ctx context.Context, arg GetAutomationsParams) ([]Automation, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAutomations,
|
||||
arg.OwnerID,
|
||||
arg.OrganizationID,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Automation
|
||||
for rows.Next() {
|
||||
var i Automation
|
||||
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)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertAutomation = `-- name: InsertAutomation :one
|
||||
INSERT INTO automations (
|
||||
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
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::text,
|
||||
$6::uuid,
|
||||
COALESCE($7::uuid[], '{}'::uuid[]),
|
||||
COALESCE($8::text[], '{}'::text[]),
|
||||
$9::text,
|
||||
$10::integer,
|
||||
$11::integer
|
||||
) RETURNING 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
|
||||
`
|
||||
|
||||
type InsertAutomationParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Instructions string `db:"instructions" json:"instructions"`
|
||||
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
AllowedTools []string `db:"allowed_tools" json:"allowed_tools"`
|
||||
Status string `db:"status" json:"status"`
|
||||
MaxChatCreatesPerHour int32 `db:"max_chat_creates_per_hour" json:"max_chat_creates_per_hour"`
|
||||
MaxMessagesPerHour int32 `db:"max_messages_per_hour" json:"max_messages_per_hour"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAutomation(ctx context.Context, arg InsertAutomationParams) (Automation, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAutomation,
|
||||
arg.OwnerID,
|
||||
arg.OrganizationID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Instructions,
|
||||
arg.ModelConfigID,
|
||||
pq.Array(arg.MCPServerIDs),
|
||||
pq.Array(arg.AllowedTools),
|
||||
arg.Status,
|
||||
arg.MaxChatCreatesPerHour,
|
||||
arg.MaxMessagesPerHour,
|
||||
)
|
||||
var i Automation
|
||||
err := row.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,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateAutomation = `-- name: UpdateAutomation :one
|
||||
UPDATE automations SET
|
||||
name = $1::text,
|
||||
description = $2::text,
|
||||
instructions = $3::text,
|
||||
model_config_id = $4::uuid,
|
||||
mcp_server_ids = $5::uuid[],
|
||||
allowed_tools = $6::text[],
|
||||
status = $7::text,
|
||||
max_chat_creates_per_hour = $8::integer,
|
||||
max_messages_per_hour = $9::integer,
|
||||
updated_at = NOW()
|
||||
WHERE id = $10::uuid
|
||||
RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateAutomationParams struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Instructions string `db:"instructions" json:"instructions"`
|
||||
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
AllowedTools []string `db:"allowed_tools" json:"allowed_tools"`
|
||||
Status string `db:"status" json:"status"`
|
||||
MaxChatCreatesPerHour int32 `db:"max_chat_creates_per_hour" json:"max_chat_creates_per_hour"`
|
||||
MaxMessagesPerHour int32 `db:"max_messages_per_hour" json:"max_messages_per_hour"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAutomation(ctx context.Context, arg UpdateAutomationParams) (Automation, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateAutomation,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Instructions,
|
||||
arg.ModelConfigID,
|
||||
pq.Array(arg.MCPServerIDs),
|
||||
pq.Array(arg.AllowedTools),
|
||||
arg.Status,
|
||||
arg.MaxChatCreatesPerHour,
|
||||
arg.MaxMessagesPerHour,
|
||||
arg.ID,
|
||||
)
|
||||
var i Automation
|
||||
err := row.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,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteAutomationTriggerByID = `-- name: DeleteAutomationTriggerByID :exec
|
||||
DELETE FROM automation_triggers WHERE id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAutomationTriggerByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getActiveCronTriggers = `-- name: GetActiveCronTriggers :many
|
||||
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 automation_triggers t
|
||||
JOIN 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')
|
||||
`
|
||||
|
||||
type GetActiveCronTriggersRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
Type string `db:"type" json:"type"`
|
||||
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
|
||||
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
|
||||
LabelPaths pqtype.NullRawMessage `db:"label_paths" json:"label_paths"`
|
||||
LastTriggeredAt sql.NullTime `db:"last_triggered_at" json:"last_triggered_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
AutomationStatus string `db:"automation_status" json:"automation_status"`
|
||||
AutomationOwnerID uuid.UUID `db:"automation_owner_id" json:"automation_owner_id"`
|
||||
AutomationInstructions string `db:"automation_instructions" json:"automation_instructions"`
|
||||
AutomationName string `db:"automation_name" json:"automation_name"`
|
||||
AutomationOrganizationID uuid.UUID `db:"automation_organization_id" json:"automation_organization_id"`
|
||||
AutomationModelConfigID uuid.NullUUID `db:"automation_model_config_id" json:"automation_model_config_id"`
|
||||
AutomationMcpServerIds []uuid.UUID `db:"automation_mcp_server_ids" json:"automation_mcp_server_ids"`
|
||||
AutomationAllowedTools []string `db:"automation_allowed_tools" json:"automation_allowed_tools"`
|
||||
AutomationMaxChatCreatesPerHour int32 `db:"automation_max_chat_creates_per_hour" json:"automation_max_chat_creates_per_hour"`
|
||||
AutomationMaxMessagesPerHour int32 `db:"automation_max_messages_per_hour" json:"automation_max_messages_per_hour"`
|
||||
}
|
||||
|
||||
// Returns all cron triggers whose parent automation is active or in
|
||||
// preview mode. The scheduler uses this to evaluate which triggers
|
||||
// are due.
|
||||
func (q *sqlQuerier) GetActiveCronTriggers(ctx context.Context) ([]GetActiveCronTriggersRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActiveCronTriggers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetActiveCronTriggersRow
|
||||
for rows.Next() {
|
||||
var i GetActiveCronTriggersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.LastTriggeredAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.AutomationStatus,
|
||||
&i.AutomationOwnerID,
|
||||
&i.AutomationInstructions,
|
||||
&i.AutomationName,
|
||||
&i.AutomationOrganizationID,
|
||||
&i.AutomationModelConfigID,
|
||||
pq.Array(&i.AutomationMcpServerIds),
|
||||
pq.Array(&i.AutomationAllowedTools),
|
||||
&i.AutomationMaxChatCreatesPerHour,
|
||||
&i.AutomationMaxMessagesPerHour,
|
||||
); 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
|
||||
}
|
||||
|
||||
const getAutomationTriggerByID = `-- name: GetAutomationTriggerByID :one
|
||||
SELECT id, automation_id, type, webhook_secret, webhook_secret_key_id, cron_schedule, filter, label_paths, created_at, updated_at, last_triggered_at FROM automation_triggers WHERE id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAutomationTriggerByID(ctx context.Context, id uuid.UUID) (AutomationTrigger, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAutomationTriggerByID, id)
|
||||
var i AutomationTrigger
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.WebhookSecret,
|
||||
&i.WebhookSecretKeyID,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastTriggeredAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAutomationTriggersByAutomationID = `-- name: GetAutomationTriggersByAutomationID :many
|
||||
SELECT id, automation_id, type, webhook_secret, webhook_secret_key_id, cron_schedule, filter, label_paths, created_at, updated_at, last_triggered_at FROM automation_triggers
|
||||
WHERE automation_id = $1::uuid
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]AutomationTrigger, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAutomationTriggersByAutomationID, automationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AutomationTrigger
|
||||
for rows.Next() {
|
||||
var i AutomationTrigger
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.WebhookSecret,
|
||||
&i.WebhookSecretKeyID,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastTriggeredAt,
|
||||
); 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
|
||||
}
|
||||
|
||||
const insertAutomationTrigger = `-- name: InsertAutomationTrigger :one
|
||||
INSERT INTO automation_triggers (
|
||||
automation_id,
|
||||
type,
|
||||
webhook_secret,
|
||||
webhook_secret_key_id,
|
||||
cron_schedule,
|
||||
filter,
|
||||
label_paths
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::text,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::text,
|
||||
$6::jsonb,
|
||||
$7::jsonb
|
||||
) RETURNING id, automation_id, type, webhook_secret, webhook_secret_key_id, cron_schedule, filter, label_paths, created_at, updated_at, last_triggered_at
|
||||
`
|
||||
|
||||
type InsertAutomationTriggerParams struct {
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
Type string `db:"type" json:"type"`
|
||||
WebhookSecret sql.NullString `db:"webhook_secret" json:"webhook_secret"`
|
||||
WebhookSecretKeyID sql.NullString `db:"webhook_secret_key_id" json:"webhook_secret_key_id"`
|
||||
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
|
||||
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
|
||||
LabelPaths pqtype.NullRawMessage `db:"label_paths" json:"label_paths"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAutomationTrigger(ctx context.Context, arg InsertAutomationTriggerParams) (AutomationTrigger, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAutomationTrigger,
|
||||
arg.AutomationID,
|
||||
arg.Type,
|
||||
arg.WebhookSecret,
|
||||
arg.WebhookSecretKeyID,
|
||||
arg.CronSchedule,
|
||||
arg.Filter,
|
||||
arg.LabelPaths,
|
||||
)
|
||||
var i AutomationTrigger
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.WebhookSecret,
|
||||
&i.WebhookSecretKeyID,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastTriggeredAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateAutomationTrigger = `-- name: UpdateAutomationTrigger :one
|
||||
UPDATE automation_triggers SET
|
||||
cron_schedule = $1::text,
|
||||
filter = $2::jsonb,
|
||||
label_paths = $3::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = $4::uuid
|
||||
RETURNING id, automation_id, type, webhook_secret, webhook_secret_key_id, cron_schedule, filter, label_paths, created_at, updated_at, last_triggered_at
|
||||
`
|
||||
|
||||
type UpdateAutomationTriggerParams struct {
|
||||
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
|
||||
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
|
||||
LabelPaths pqtype.NullRawMessage `db:"label_paths" json:"label_paths"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAutomationTrigger(ctx context.Context, arg UpdateAutomationTriggerParams) (AutomationTrigger, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateAutomationTrigger,
|
||||
arg.CronSchedule,
|
||||
arg.Filter,
|
||||
arg.LabelPaths,
|
||||
arg.ID,
|
||||
)
|
||||
var i AutomationTrigger
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.WebhookSecret,
|
||||
&i.WebhookSecretKeyID,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastTriggeredAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateAutomationTriggerLastTriggeredAt = `-- name: UpdateAutomationTriggerLastTriggeredAt :exec
|
||||
UPDATE automation_triggers
|
||||
SET last_triggered_at = $1::timestamptz
|
||||
WHERE id = $2::uuid
|
||||
`
|
||||
|
||||
type UpdateAutomationTriggerLastTriggeredAtParams struct {
|
||||
LastTriggeredAt time.Time `db:"last_triggered_at" json:"last_triggered_at"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAutomationTriggerLastTriggeredAt(ctx context.Context, arg UpdateAutomationTriggerLastTriggeredAtParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAutomationTriggerLastTriggeredAt, arg.LastTriggeredAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateAutomationTriggerWebhookSecret = `-- name: UpdateAutomationTriggerWebhookSecret :one
|
||||
UPDATE automation_triggers SET
|
||||
webhook_secret = $1::text,
|
||||
webhook_secret_key_id = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE id = $3::uuid
|
||||
RETURNING id, automation_id, type, webhook_secret, webhook_secret_key_id, cron_schedule, filter, label_paths, created_at, updated_at, last_triggered_at
|
||||
`
|
||||
|
||||
type UpdateAutomationTriggerWebhookSecretParams struct {
|
||||
WebhookSecret sql.NullString `db:"webhook_secret" json:"webhook_secret"`
|
||||
WebhookSecretKeyID sql.NullString `db:"webhook_secret_key_id" json:"webhook_secret_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAutomationTriggerWebhookSecret(ctx context.Context, arg UpdateAutomationTriggerWebhookSecretParams) (AutomationTrigger, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateAutomationTriggerWebhookSecret, arg.WebhookSecret, arg.WebhookSecretKeyID, arg.ID)
|
||||
var i AutomationTrigger
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutomationID,
|
||||
&i.Type,
|
||||
&i.WebhookSecret,
|
||||
&i.WebhookSecretKeyID,
|
||||
&i.CronSchedule,
|
||||
&i.Filter,
|
||||
&i.LabelPaths,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastTriggeredAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAndResetBoundaryUsageSummary = `-- name: GetAndResetBoundaryUsageSummary :one
|
||||
WITH deleted AS (
|
||||
DELETE FROM boundary_usage_stats
|
||||
@@ -3823,7 +4566,7 @@ WHERE
|
||||
$3::int
|
||||
)
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type AcquireChatsParams struct {
|
||||
@@ -3862,6 +4605,7 @@ func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) (
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -4095,7 +4839,7 @@ func (q *sqlQuerier) DeleteChatUsageLimitUserOverride(ctx context.Context, userI
|
||||
|
||||
const getChatByID = `-- name: GetChatByID :one
|
||||
SELECT
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
@@ -4124,12 +4868,13 @@ func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels FROM chats WHERE id = $1::uuid FOR UPDATE
|
||||
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id FROM chats WHERE id = $1::uuid FOR UPDATE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) {
|
||||
@@ -4154,6 +4899,7 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -4998,7 +5744,7 @@ func (q *sqlQuerier) GetChatUsageLimitUserOverride(ctx context.Context, userID u
|
||||
|
||||
const getChats = `-- name: GetChats :many
|
||||
SELECT
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
@@ -5089,6 +5835,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]Chat,
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -5154,7 +5901,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
|
||||
|
||||
const getStaleChats = `-- name: GetStaleChats :many
|
||||
SELECT
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
@@ -5192,6 +5939,7 @@ func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -5269,7 +6017,7 @@ INSERT INTO chats (
|
||||
COALESCE($9::jsonb, '{}'::jsonb)
|
||||
)
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type InsertChatParams struct {
|
||||
@@ -5316,6 +6064,7 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -5711,7 +6460,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type UpdateChatByIDParams struct {
|
||||
@@ -5741,6 +6490,7 @@ func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParam
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -5780,7 +6530,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type UpdateChatLabelsByIDParams struct {
|
||||
@@ -5810,6 +6560,7 @@ func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLab
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -5823,7 +6574,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type UpdateChatMCPServerIDsParams struct {
|
||||
@@ -5853,6 +6604,7 @@ func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatM
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -5917,7 +6669,7 @@ SET
|
||||
WHERE
|
||||
id = $6::uuid
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type UpdateChatStatusParams struct {
|
||||
@@ -5958,6 +6710,7 @@ func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusP
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -5971,7 +6724,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, automation_id
|
||||
`
|
||||
|
||||
type UpdateChatWorkspaceParams struct {
|
||||
@@ -6001,6 +6754,7 @@ func (q *sqlQuerier) UpdateChatWorkspace(ctx context.Context, arg UpdateChatWork
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
&i.AutomationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- name: InsertAutomationEvent :one
|
||||
INSERT INTO automation_events (
|
||||
automation_id,
|
||||
trigger_id,
|
||||
payload,
|
||||
filter_matched,
|
||||
resolved_labels,
|
||||
matched_chat_id,
|
||||
created_chat_id,
|
||||
status,
|
||||
error
|
||||
) VALUES (
|
||||
@automation_id::uuid,
|
||||
sqlc.narg('trigger_id')::uuid,
|
||||
@payload::jsonb,
|
||||
@filter_matched::boolean,
|
||||
sqlc.narg('resolved_labels')::jsonb,
|
||||
sqlc.narg('matched_chat_id')::uuid,
|
||||
sqlc.narg('created_chat_id')::uuid,
|
||||
@status::text,
|
||||
sqlc.narg('error')::text
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetAutomationEvents :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
automation_events
|
||||
WHERE
|
||||
automation_id = @automation_id::uuid
|
||||
AND CASE
|
||||
WHEN sqlc.narg('status_filter')::text IS NOT NULL THEN status = sqlc.narg('status_filter')::text
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY
|
||||
received_at DESC
|
||||
OFFSET @offset_opt
|
||||
LIMIT
|
||||
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
|
||||
|
||||
-- name: CountAutomationChatCreatesInWindow :one
|
||||
SELECT COUNT(*)
|
||||
FROM automation_events
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
AND status = 'created'
|
||||
AND received_at > @window_start::timestamptz;
|
||||
|
||||
-- name: CountAutomationMessagesInWindow :one
|
||||
SELECT COUNT(*)
|
||||
FROM automation_events
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
AND status IN ('created', 'continued')
|
||||
AND received_at > @window_start::timestamptz;
|
||||
|
||||
-- name: PurgeOldAutomationEvents :exec
|
||||
DELETE FROM automation_events
|
||||
WHERE received_at < NOW() - INTERVAL '7 days';
|
||||
@@ -0,0 +1,69 @@
|
||||
-- name: InsertAutomation :one
|
||||
INSERT INTO automations (
|
||||
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
|
||||
) VALUES (
|
||||
@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::text,
|
||||
@max_chat_creates_per_hour::integer,
|
||||
@max_messages_per_hour::integer
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetAutomationByID :one
|
||||
SELECT * FROM automations WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetAutomations :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
automations
|
||||
WHERE
|
||||
CASE
|
||||
WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN automations.owner_id = @owner_id
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN automations.organization_id = @organization_id
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedAutomations
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
OFFSET @offset_opt
|
||||
LIMIT
|
||||
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
|
||||
|
||||
-- name: UpdateAutomation :one
|
||||
UPDATE automations SET
|
||||
name = @name::text,
|
||||
description = @description::text,
|
||||
instructions = @instructions::text,
|
||||
model_config_id = sqlc.narg('model_config_id')::uuid,
|
||||
mcp_server_ids = @mcp_server_ids::uuid[],
|
||||
allowed_tools = @allowed_tools::text[],
|
||||
status = @status::text,
|
||||
max_chat_creates_per_hour = @max_chat_creates_per_hour::integer,
|
||||
max_messages_per_hour = @max_messages_per_hour::integer,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteAutomationByID :exec
|
||||
DELETE FROM automations WHERE id = @id::uuid;
|
||||
@@ -0,0 +1,81 @@
|
||||
-- name: InsertAutomationTrigger :one
|
||||
INSERT INTO automation_triggers (
|
||||
automation_id,
|
||||
type,
|
||||
webhook_secret,
|
||||
webhook_secret_key_id,
|
||||
cron_schedule,
|
||||
filter,
|
||||
label_paths
|
||||
) VALUES (
|
||||
@automation_id::uuid,
|
||||
@type::text,
|
||||
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
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetAutomationTriggerByID :one
|
||||
SELECT * FROM automation_triggers WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetAutomationTriggersByAutomationID :many
|
||||
SELECT * FROM automation_triggers
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: UpdateAutomationTrigger :one
|
||||
UPDATE automation_triggers SET
|
||||
cron_schedule = sqlc.narg('cron_schedule')::text,
|
||||
filter = sqlc.narg('filter')::jsonb,
|
||||
label_paths = sqlc.narg('label_paths')::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateAutomationTriggerWebhookSecret :one
|
||||
UPDATE automation_triggers SET
|
||||
webhook_secret = sqlc.narg('webhook_secret')::text,
|
||||
webhook_secret_key_id = sqlc.narg('webhook_secret_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteAutomationTriggerByID :exec
|
||||
DELETE FROM automation_triggers WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetActiveCronTriggers :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 automation_triggers t
|
||||
JOIN 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: UpdateAutomationTriggerLastTriggeredAt :exec
|
||||
UPDATE automation_triggers
|
||||
SET last_triggered_at = @last_triggered_at::timestamptz
|
||||
WHERE id = @id::uuid;
|
||||
@@ -247,6 +247,7 @@ sql:
|
||||
mcp_server_tool_snapshots: MCPServerToolSnapshots
|
||||
mcp_server_config_id: MCPServerConfigID
|
||||
mcp_server_ids: MCPServerIDs
|
||||
webhook_secret_key_id: WebhookSecretKeyID
|
||||
icon_url: IconURL
|
||||
oauth2_client_id: OAuth2ClientID
|
||||
oauth2_client_secret: OAuth2ClientSecret
|
||||
|
||||
@@ -14,6 +14,9 @@ const (
|
||||
UniqueAibridgeUserPromptsPkey UniqueConstraint = "aibridge_user_prompts_pkey" // ALTER TABLE ONLY aibridge_user_prompts ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id);
|
||||
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);
|
||||
UniqueAutomationEventsPkey UniqueConstraint = "automation_events_pkey" // ALTER TABLE ONLY automation_events ADD CONSTRAINT automation_events_pkey PRIMARY KEY (id);
|
||||
UniqueAutomationTriggersPkey UniqueConstraint = "automation_triggers_pkey" // ALTER TABLE ONLY automation_triggers ADD CONSTRAINT automation_triggers_pkey PRIMARY KEY (id);
|
||||
UniqueAutomationsPkey UniqueConstraint = "automations_pkey" // ALTER TABLE ONLY automations ADD CONSTRAINT automations_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);
|
||||
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);
|
||||
@@ -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);
|
||||
UniqueIndexAutomationsOwnerOrgName UniqueConstraint = "idx_automations_owner_org_name" // CREATE UNIQUE INDEX idx_automations_owner_org_name ON 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));
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/automations"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func generateWebhookSecret() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", xerrors.Errorf("generate webhook secret: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// postAutomation creates a new automation.
|
||||
func (api *API) postAutomation(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
var req codersdk.CreateAutomationRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Name is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.OrganizationID == uuid.Nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "organization_id is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
maxCreates := int32(10)
|
||||
if req.MaxChatCreatesPerHour != nil {
|
||||
maxCreates = *req.MaxChatCreatesPerHour
|
||||
}
|
||||
maxMessages := int32(60)
|
||||
if req.MaxMessagesPerHour != nil {
|
||||
maxMessages = *req.MaxMessagesPerHour
|
||||
}
|
||||
|
||||
if maxCreates <= 0 || maxCreates > 1000 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "max_chat_creates_per_hour must be between 1 and 1000.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if maxMessages <= 0 || maxMessages > 1000 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "max_messages_per_hour must be between 1 and 1000.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
arg := database.InsertAutomationParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
OrganizationID: req.OrganizationID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Instructions: req.Instructions,
|
||||
MCPServerIDs: req.MCPServerIDs,
|
||||
AllowedTools: req.AllowedTools,
|
||||
Status: "disabled",
|
||||
MaxChatCreatesPerHour: maxCreates,
|
||||
MaxMessagesPerHour: maxMessages,
|
||||
}
|
||||
if req.ModelConfigID != nil {
|
||||
arg.ModelConfigID = uuid.NullUUID{UUID: *req.ModelConfigID, Valid: true}
|
||||
}
|
||||
|
||||
automation, err := api.Database.InsertAutomation(ctx, arg)
|
||||
if err != nil {
|
||||
switch {
|
||||
case database.IsUniqueViolation(err, database.UniqueIndexAutomationsOwnerOrgName):
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "An automation with this name already exists.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
case database.IsForeignKeyViolation(err):
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid organization_id or model_config_id.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to create automation.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.Automation(automation))
|
||||
}
|
||||
|
||||
// listAutomations returns all automations visible to the user.
|
||||
func (api *API) listAutomations(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
dbAutomations, err := api.Database.GetAutomations(ctx, database.GetAutomationsParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to list automations.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]codersdk.Automation, 0, len(dbAutomations))
|
||||
for _, a := range dbAutomations {
|
||||
result = append(result, db2sdk.Automation(a))
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// getAutomation returns a single automation.
|
||||
func (api *API) getAutomation(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Automation(automation))
|
||||
}
|
||||
|
||||
// patchAutomation updates an automation's configuration.
|
||||
func (api *API) patchAutomation(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
var req codersdk.UpdateAutomationRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil && *req.Name == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Name cannot be empty.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.Status != nil {
|
||||
switch *req.Status {
|
||||
case codersdk.AutomationStatusDisabled, codersdk.AutomationStatusPreview, codersdk.AutomationStatusActive:
|
||||
// Valid.
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid status %q.", *req.Status),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.MaxChatCreatesPerHour != nil && (*req.MaxChatCreatesPerHour <= 0 || *req.MaxChatCreatesPerHour > 1000) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "max_chat_creates_per_hour must be between 1 and 1000.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.MaxMessagesPerHour != nil && (*req.MaxMessagesPerHour <= 0 || *req.MaxMessagesPerHour > 1000) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "max_messages_per_hour must be between 1 and 1000.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Merge: start from current values, apply updates.
|
||||
arg := database.UpdateAutomationParams{
|
||||
ID: automation.ID,
|
||||
Name: automation.Name,
|
||||
Description: automation.Description,
|
||||
Instructions: automation.Instructions,
|
||||
ModelConfigID: automation.ModelConfigID,
|
||||
MCPServerIDs: automation.MCPServerIDs,
|
||||
AllowedTools: automation.AllowedTools,
|
||||
Status: automation.Status,
|
||||
MaxChatCreatesPerHour: automation.MaxChatCreatesPerHour,
|
||||
MaxMessagesPerHour: automation.MaxMessagesPerHour,
|
||||
}
|
||||
if req.Name != nil {
|
||||
arg.Name = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
arg.Description = *req.Description
|
||||
}
|
||||
if req.Instructions != nil {
|
||||
arg.Instructions = *req.Instructions
|
||||
}
|
||||
if req.ModelConfigID != nil {
|
||||
arg.ModelConfigID = uuid.NullUUID{UUID: *req.ModelConfigID, Valid: true}
|
||||
}
|
||||
if req.MCPServerIDs != nil {
|
||||
arg.MCPServerIDs = *req.MCPServerIDs
|
||||
}
|
||||
if req.AllowedTools != nil {
|
||||
arg.AllowedTools = *req.AllowedTools
|
||||
}
|
||||
if req.Status != nil {
|
||||
arg.Status = string(*req.Status)
|
||||
}
|
||||
if req.MaxChatCreatesPerHour != nil {
|
||||
arg.MaxChatCreatesPerHour = *req.MaxChatCreatesPerHour
|
||||
}
|
||||
if req.MaxMessagesPerHour != nil {
|
||||
arg.MaxMessagesPerHour = *req.MaxMessagesPerHour
|
||||
}
|
||||
|
||||
updated, err := api.Database.UpdateAutomation(ctx, arg)
|
||||
if err != nil {
|
||||
switch {
|
||||
case database.IsUniqueViolation(err, database.UniqueIndexAutomationsOwnerOrgName):
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "An automation with this name already exists.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to update automation.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Automation(updated))
|
||||
}
|
||||
|
||||
// deleteAutomation deletes an automation.
|
||||
func (api *API) deleteAutomation(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
err := api.Database.DeleteAutomationByID(ctx, automation.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to delete automation.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// listAutomationEvents returns recent events for an automation.
|
||||
func (api *API) listAutomationEvents(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
events, err := api.Database.GetAutomationEvents(ctx, database.GetAutomationEventsParams{
|
||||
AutomationID: automation.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to list automation events.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]codersdk.AutomationEvent, 0, len(events))
|
||||
for _, e := range events {
|
||||
result = append(result, db2sdk.AutomationEvent(e))
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// testAutomation performs a dry-run of the filter and session
|
||||
// resolution logic against a sample payload. Filter and label_paths
|
||||
// are accepted in the request body so callers can test without an
|
||||
// existing trigger.
|
||||
func (api *API) testAutomation(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
var body codersdk.TestAutomationRequest
|
||||
if !httpapi.Read(ctx, rw, r, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
payloadStr := string(body.Payload)
|
||||
matched := automations.MatchFilter(payloadStr, body.Filter)
|
||||
|
||||
result := codersdk.AutomationTestResult{
|
||||
FilterMatched: matched,
|
||||
}
|
||||
|
||||
// If filter matched and label_paths are provided, resolve them.
|
||||
if matched && len(body.LabelPaths) > 0 {
|
||||
var labelPaths map[string]string
|
||||
if err := json.Unmarshal(body.LabelPaths, &labelPaths); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid label_paths: must be a JSON object mapping label names to gjson paths.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resolvedLabels := automations.ResolveLabels(payloadStr, labelPaths)
|
||||
if labelsJSON, err := json.Marshal(resolvedLabels); err == nil {
|
||||
result.ResolvedLabels = labelsJSON
|
||||
}
|
||||
|
||||
// Look for existing chat with these labels.
|
||||
if len(resolvedLabels) > 0 {
|
||||
labelsJSON, _ := json.Marshal(resolvedLabels)
|
||||
chats, err := api.Database.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: automation.OwnerID,
|
||||
LabelFilter: pqtype.NullRawMessage{
|
||||
RawMessage: labelsJSON,
|
||||
Valid: true,
|
||||
},
|
||||
LimitOpt: 1,
|
||||
})
|
||||
if err == nil && len(chats) > 0 {
|
||||
result.ExistingChatID = &chats[0].ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.WouldCreateChat = matched && result.ExistingChatID == nil
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// postAutomationTrigger creates a new trigger for an automation.
|
||||
func (api *API) postAutomationTrigger(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
var req codersdk.CreateAutomationTriggerRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Type is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if req.Type != codersdk.AutomationTriggerTypeWebhook && req.Type != codersdk.AutomationTriggerTypeCron {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid trigger type %q. Must be %q or %q.", req.Type, codersdk.AutomationTriggerTypeWebhook, codersdk.AutomationTriggerTypeCron),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
arg := database.InsertAutomationTriggerParams{
|
||||
AutomationID: automation.ID,
|
||||
Type: string(req.Type),
|
||||
}
|
||||
|
||||
// Generate webhook secret for webhook triggers.
|
||||
if req.Type == codersdk.AutomationTriggerTypeWebhook {
|
||||
secret, err := generateWebhookSecret()
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to generate webhook secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
arg.WebhookSecret = sql.NullString{String: secret, Valid: true}
|
||||
}
|
||||
|
||||
if req.Type == codersdk.AutomationTriggerTypeCron {
|
||||
if req.CronSchedule == nil || *req.CronSchedule == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cron schedule is required for cron triggers.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if _, err := cron.Standard(*req.CronSchedule); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid cron schedule.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.CronSchedule != nil {
|
||||
arg.CronSchedule = sql.NullString{String: *req.CronSchedule, Valid: true}
|
||||
}
|
||||
if len(req.Filter) > 0 {
|
||||
var filterObj map[string]any
|
||||
if err := json.Unmarshal(req.Filter, &filterObj); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "filter must be a JSON object mapping gjson paths to expected values.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
arg.Filter = pqtype.NullRawMessage{RawMessage: req.Filter, Valid: true}
|
||||
}
|
||||
if len(req.LabelPaths) > 0 {
|
||||
var labelPathsObj map[string]string
|
||||
if err := json.Unmarshal(req.LabelPaths, &labelPathsObj); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "label_paths must be a JSON object mapping label names to gjson paths.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
arg.LabelPaths = pqtype.NullRawMessage{RawMessage: req.LabelPaths, Valid: true}
|
||||
}
|
||||
|
||||
trigger, err := api.Database.InsertAutomationTrigger(ctx, arg)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to create trigger.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := db2sdk.AutomationTrigger(trigger, api.AccessURL.String())
|
||||
// Reveal the secret on creation so the user can configure
|
||||
// their webhook provider. This is the only time the plaintext
|
||||
// secret is included in a response.
|
||||
if trigger.WebhookSecret.Valid {
|
||||
result.WebhookSecret = trigger.WebhookSecret.String
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// listAutomationTriggers lists triggers for an automation.
|
||||
func (api *API) listAutomationTriggers(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
triggers, err := api.Database.GetAutomationTriggersByAutomationID(ctx, automation.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to list triggers.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]codersdk.AutomationTrigger, 0, len(triggers))
|
||||
for _, t := range triggers {
|
||||
result = append(result, db2sdk.AutomationTrigger(t, api.AccessURL.String()))
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// deleteAutomationTrigger deletes a trigger from an automation.
|
||||
func (api *API) deleteAutomationTrigger(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
triggerID, parsed := httpmw.ParseUUIDParam(rw, r, "trigger")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the trigger belongs to this automation.
|
||||
trigger, err := api.Database.GetAutomationTriggerByID(ctx, triggerID)
|
||||
if err != nil || trigger.AutomationID != automation.ID {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.DeleteAutomationTriggerByID(ctx, triggerID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to delete trigger.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// regenerateAutomationTriggerSecret generates a new webhook secret for
|
||||
// a trigger.
|
||||
func (api *API) regenerateAutomationTriggerSecret(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automation := httpmw.AutomationParam(r)
|
||||
|
||||
triggerID, parsed := httpmw.ParseUUIDParam(rw, r, "trigger")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the trigger belongs to this automation.
|
||||
trigger, err := api.Database.GetAutomationTriggerByID(ctx, triggerID)
|
||||
if err != nil || trigger.AutomationID != automation.ID {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
if trigger.Type != "webhook" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Can only regenerate secrets for webhook triggers.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
secret, err := generateWebhookSecret()
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to generate webhook secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := api.Database.UpdateAutomationTriggerWebhookSecret(ctx, database.UpdateAutomationTriggerWebhookSecretParams{
|
||||
ID: triggerID,
|
||||
WebhookSecret: sql.NullString{String: secret, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to update webhook secret.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := db2sdk.AutomationTrigger(updated, api.AccessURL.String())
|
||||
// Reveal the new secret so the user can reconfigure their
|
||||
// webhook provider.
|
||||
if updated.WebhookSecret.Valid {
|
||||
result.WebhookSecret = updated.WebhookSecret.String
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// postAutomationWebhook is the unauthenticated stable v2 endpoint
|
||||
// that receives webhook deliveries from external systems. The URL
|
||||
// identifies a specific trigger rather than an automation.
|
||||
func (api *API) postAutomationWebhook(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
triggerIDStr := chi.URLParam(r, "trigger_id")
|
||||
triggerID, err := uuid.Parse(triggerIDStr)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Respect the experiment flag even on the unauthenticated
|
||||
// webhook path. We check here rather than in middleware
|
||||
// because the endpoint must always return 200.
|
||||
if !api.Experiments.Enabled(codersdk.ExperimentAgents) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Always return 200 to prevent source-system retries.
|
||||
//nolint:gocritic // Webhook handler must bypass auth to look up trigger.
|
||||
trigger, err := api.Database.GetAutomationTriggerByID(dbauthz.AsSystemRestricted(ctx), triggerID)
|
||||
if err != nil {
|
||||
// Still return 200 even if trigger not found.
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:gocritic // Webhook handler must bypass auth to look up automation.
|
||||
automation, err := api.Database.GetAutomationByID(dbauthz.AsSystemRestricted(ctx), trigger.AutomationID)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if trigger.Type != "webhook" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if automation.Status == "disabled" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Read body with size limit.
|
||||
r.Body = http.MaxBytesReader(rw, r.Body, 256*1024) // 256 KB
|
||||
payload, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify HMAC signature.
|
||||
sig := r.Header.Get("X-Hub-Signature-256")
|
||||
if !automations.VerifySignature(payload, trigger.WebhookSecret.String, sig) {
|
||||
// Log event as error but still return 200.
|
||||
//nolint:gocritic // System context for event logging.
|
||||
_, _ = api.Database.InsertAutomationEvent(dbauthz.AsSystemRestricted(ctx), database.InsertAutomationEventParams{
|
||||
AutomationID: automation.ID,
|
||||
TriggerID: uuid.NullUUID{UUID: trigger.ID, Valid: true},
|
||||
Payload: safePayload(payload),
|
||||
FilterMatched: false,
|
||||
Status: "error",
|
||||
Error: sql.NullString{String: "signature verification failed", Valid: true},
|
||||
})
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
payloadStr := string(payload)
|
||||
matched := automations.MatchFilter(payloadStr, trigger.Filter.RawMessage)
|
||||
|
||||
if !matched {
|
||||
//nolint:gocritic // System context for event logging.
|
||||
_, _ = api.Database.InsertAutomationEvent(dbauthz.AsSystemRestricted(ctx), database.InsertAutomationEventParams{
|
||||
AutomationID: automation.ID,
|
||||
TriggerID: uuid.NullUUID{UUID: trigger.ID, Valid: true},
|
||||
Payload: safePayload(payload),
|
||||
FilterMatched: false,
|
||||
Status: "filtered",
|
||||
})
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve labels.
|
||||
var resolvedLabels map[string]string
|
||||
if trigger.LabelPaths.Valid {
|
||||
var labelPaths map[string]string
|
||||
if err := json.Unmarshal(trigger.LabelPaths.RawMessage, &labelPaths); err == nil {
|
||||
resolvedLabels = automations.ResolveLabels(payloadStr, labelPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// Create or continue a chat (or log preview event).
|
||||
fireOpts := automations.FireOptions{
|
||||
AutomationID: automation.ID,
|
||||
AutomationName: automation.Name,
|
||||
AutomationStatus: automation.Status,
|
||||
AutomationOwnerID: automation.OwnerID,
|
||||
AutomationInstructions: automation.Instructions,
|
||||
AutomationModelConfigID: automation.ModelConfigID,
|
||||
AutomationMCPServerIDs: automation.MCPServerIDs,
|
||||
AutomationAllowedTools: automation.AllowedTools,
|
||||
MaxChatCreatesPerHour: automation.MaxChatCreatesPerHour,
|
||||
MaxMessagesPerHour: automation.MaxMessagesPerHour,
|
||||
TriggerID: trigger.ID,
|
||||
Payload: safePayload(payload),
|
||||
FilterMatched: true,
|
||||
ResolvedLabels: resolvedLabels,
|
||||
}
|
||||
|
||||
chatAdapter := &automations.ChatdAdapter{Server: api.chatDaemon}
|
||||
//nolint:gocritic // System context for automation execution.
|
||||
automations.Fire(dbauthz.AsSystemRestricted(ctx), api.Logger, api.Database, chatAdapter, fireOpts)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// safePayload ensures the payload is valid JSON before storing
|
||||
// it in the jsonb column. If the payload is not valid JSON, it
|
||||
// is wrapped in a valid JSON envelope so the event audit trail
|
||||
// is preserved. The inner body is truncated before wrapping to
|
||||
// ensure the envelope fits within the storage limit.
|
||||
func safePayload(payload []byte) json.RawMessage {
|
||||
if json.Valid(payload) {
|
||||
return truncatePayload(payload)
|
||||
}
|
||||
// Reserve space for the JSON envelope keys and structure.
|
||||
// {"_raw_body":"...","_content_note":"Original payload was not valid JSON."}
|
||||
const envelopeOverhead = 128
|
||||
const maxPayloadSize = 64 * 1024
|
||||
innerLimit := maxPayloadSize - envelopeOverhead
|
||||
body := string(payload)
|
||||
if len(body) > innerLimit {
|
||||
body = body[:innerLimit]
|
||||
}
|
||||
wrapped, err := json.Marshal(map[string]any{
|
||||
"_raw_body": body,
|
||||
"_content_note": "Original payload was not valid JSON.",
|
||||
})
|
||||
if err != nil {
|
||||
return json.RawMessage(`{"_error":"failed to encode payload"}`)
|
||||
}
|
||||
// The wrapped result should fit within maxPayloadSize, but
|
||||
// JSON string escaping can expand the body. Apply truncation
|
||||
// as a safety net.
|
||||
return truncatePayload(wrapped)
|
||||
}
|
||||
|
||||
// truncatePayload limits the stored payload to 64 KB. If the
|
||||
// payload exceeds the limit, a valid JSON stub is returned
|
||||
// instead of slicing the original bytes (which would produce
|
||||
// syntactically broken JSON).
|
||||
func truncatePayload(payload []byte) json.RawMessage {
|
||||
const maxPayloadSize = 64 * 1024
|
||||
if len(payload) <= maxPayloadSize {
|
||||
return json.RawMessage(payload)
|
||||
}
|
||||
// Produce valid JSON indicating the payload was truncated.
|
||||
return json.RawMessage(fmt.Sprintf(
|
||||
`{"_truncated":true,"_original_size":%d}`,
|
||||
len(payload),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
type automationParamContextKey struct{}
|
||||
|
||||
// AutomationParam returns the automation from the
|
||||
// ExtractAutomationParam handler.
|
||||
func AutomationParam(r *http.Request) database.Automation {
|
||||
automation, ok := r.Context().Value(automationParamContextKey{}).(database.Automation)
|
||||
if !ok {
|
||||
panic("developer error: automation param middleware not provided")
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
// ExtractAutomationParam grabs an automation from the "automation" URL
|
||||
// parameter.
|
||||
func ExtractAutomationParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
automationID, parsed := ParseUUIDParam(rw, r, "automation")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
|
||||
automation, err := db.GetAutomationByID(ctx, automationID)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching automation.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, automationParamContextKey{}, automation)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,16 @@ var (
|
||||
Type: "audit_log",
|
||||
}
|
||||
|
||||
// ResourceAutomation
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an automation
|
||||
// - "ActionDelete" :: delete an automation
|
||||
// - "ActionRead" :: read automation configuration
|
||||
// - "ActionUpdate" :: update an automation
|
||||
ResourceAutomation = Object{
|
||||
Type: "automation",
|
||||
}
|
||||
|
||||
// ResourceBoundaryUsage
|
||||
// Valid Actions
|
||||
// - "ActionDelete" :: delete boundary usage statistics
|
||||
@@ -438,6 +448,7 @@ func AllResources() []Objecter {
|
||||
ResourceAssignOrgRole,
|
||||
ResourceAssignRole,
|
||||
ResourceAuditLog,
|
||||
ResourceAutomation,
|
||||
ResourceBoundaryUsage,
|
||||
ResourceChat,
|
||||
ResourceConnectionLog,
|
||||
|
||||
@@ -84,6 +84,13 @@ var chatActions = map[Action]ActionDefinition{
|
||||
ActionDelete: "delete a chat",
|
||||
}
|
||||
|
||||
var automationActions = map[Action]ActionDefinition{
|
||||
ActionCreate: "create an automation",
|
||||
ActionRead: "read automation configuration",
|
||||
ActionUpdate: "update an automation",
|
||||
ActionDelete: "delete an 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,
|
||||
},
|
||||
"automation": {
|
||||
Actions: automationActions,
|
||||
},
|
||||
// Dormant workspaces have the same perms as workspaces.
|
||||
"workspace_dormant": {
|
||||
Actions: workspaceActions,
|
||||
|
||||
@@ -25,6 +25,10 @@ const (
|
||||
ScopeAssignRoleUnassign ScopeName = "assign_role:unassign"
|
||||
ScopeAuditLogCreate ScopeName = "audit_log:create"
|
||||
ScopeAuditLogRead ScopeName = "audit_log:read"
|
||||
ScopeAutomationCreate ScopeName = "automation:create"
|
||||
ScopeAutomationDelete ScopeName = "automation:delete"
|
||||
ScopeAutomationRead ScopeName = "automation:read"
|
||||
ScopeAutomationUpdate ScopeName = "automation:update"
|
||||
ScopeBoundaryUsageDelete ScopeName = "boundary_usage:delete"
|
||||
ScopeBoundaryUsageRead ScopeName = "boundary_usage:read"
|
||||
ScopeBoundaryUsageUpdate ScopeName = "boundary_usage:update"
|
||||
@@ -189,6 +193,10 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeAssignRoleUnassign,
|
||||
ScopeAuditLogCreate,
|
||||
ScopeAuditLogRead,
|
||||
ScopeAutomationCreate,
|
||||
ScopeAutomationDelete,
|
||||
ScopeAutomationRead,
|
||||
ScopeAutomationUpdate,
|
||||
ScopeBoundaryUsageDelete,
|
||||
ScopeBoundaryUsageRead,
|
||||
ScopeBoundaryUsageUpdate,
|
||||
@@ -354,6 +362,10 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeAssignRoleUnassign,
|
||||
ScopeAuditLogCreate,
|
||||
ScopeAuditLogRead,
|
||||
ScopeAutomationCreate,
|
||||
ScopeAutomationDelete,
|
||||
ScopeAutomationRead,
|
||||
ScopeAutomationUpdate,
|
||||
ScopeBoundaryUsageDelete,
|
||||
ScopeBoundaryUsageRead,
|
||||
ScopeBoundaryUsageUpdate,
|
||||
|
||||
@@ -86,6 +86,29 @@ func Daily(raw string) (*Schedule, error) {
|
||||
//
|
||||
// Unlike standard cron, this function interprets the input as a continuous active period
|
||||
// rather than discrete scheduled times.
|
||||
// Standard parses a Schedule from a full 5-field cron spec without
|
||||
// additional constraints on which fields may use wildcards. All five
|
||||
// fields (minute, hour, day-of-month, month, day-of-week) accept any
|
||||
// valid cron expression including ranges, steps, and lists.
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// sched, _ := cron.Standard("*/5 * * * *") // every 5 minutes
|
||||
// sched, _ := cron.Standard("0 9 * * 1-5") // 9 AM weekdays
|
||||
// sched, _ := cron.Standard("CRON_TZ=US/Central 30 8 1 * *") // 8:30 AM Central on the 1st
|
||||
func Standard(raw string) (*Schedule, error) {
|
||||
// Validate that the spec has the right number of fields.
|
||||
parts := strings.Fields(raw)
|
||||
expected := 5
|
||||
if len(parts) > 0 && strings.HasPrefix(parts[0], "CRON_TZ=") {
|
||||
expected = 6
|
||||
}
|
||||
if len(parts) != expected {
|
||||
return nil, xerrors.Errorf("expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix")
|
||||
}
|
||||
return parse(raw)
|
||||
}
|
||||
|
||||
func TimeRange(raw string) (*Schedule, error) {
|
||||
if err := validateTimeRangeSpec(raw); err != nil {
|
||||
return nil, xerrors.Errorf("validate time range schedule: %w", err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
@@ -281,3 +282,139 @@ func mustLocation(t *testing.T, s string) *time.Location {
|
||||
require.NoError(t, err)
|
||||
return loc
|
||||
}
|
||||
|
||||
func TestStandard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("valid specs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
spec string
|
||||
expectedCron string
|
||||
expectedLocation *time.Location
|
||||
expectedString string
|
||||
}{
|
||||
{
|
||||
name: "every minute",
|
||||
spec: "* * * * *",
|
||||
expectedCron: "* * * * *",
|
||||
expectedLocation: time.UTC,
|
||||
expectedString: "CRON_TZ=UTC * * * * *",
|
||||
},
|
||||
{
|
||||
name: "every 5 minutes",
|
||||
spec: "*/5 * * * *",
|
||||
expectedCron: "*/5 * * * *",
|
||||
expectedLocation: time.UTC,
|
||||
expectedString: "CRON_TZ=UTC */5 * * * *",
|
||||
},
|
||||
{
|
||||
name: "9 AM weekdays",
|
||||
spec: "0 9 * * 1-5",
|
||||
expectedCron: "0 9 * * 1-5",
|
||||
expectedLocation: time.UTC,
|
||||
expectedString: "CRON_TZ=UTC 0 9 * * 1-5",
|
||||
},
|
||||
{
|
||||
name: "8:30 AM on the 1st of every month",
|
||||
spec: "30 8 1 * *",
|
||||
expectedCron: "30 8 1 * *",
|
||||
expectedLocation: time.UTC,
|
||||
expectedString: "CRON_TZ=UTC 30 8 1 * *",
|
||||
},
|
||||
{
|
||||
name: "midnight every Sunday",
|
||||
spec: "0 0 * * 0",
|
||||
expectedCron: "0 0 * * 0",
|
||||
expectedLocation: time.UTC,
|
||||
expectedString: "CRON_TZ=UTC 0 0 * * 0",
|
||||
},
|
||||
{
|
||||
name: "with timezone",
|
||||
spec: "CRON_TZ=US/Central 30 9 * * 1-5",
|
||||
expectedCron: "30 9 * * 1-5",
|
||||
expectedLocation: mustLocation(t, "US/Central"),
|
||||
expectedString: "CRON_TZ=US/Central 30 9 * * 1-5",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sched, err := cron.Standard(tc.spec)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedCron, sched.Cron())
|
||||
assert.Equal(t, tc.expectedLocation, sched.Location())
|
||||
assert.Equal(t, tc.expectedString, sched.String())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid specs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
spec string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
spec: "",
|
||||
expectedError: "expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||
},
|
||||
{
|
||||
name: "garbage",
|
||||
spec: "not a cron",
|
||||
expectedError: "expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||
},
|
||||
{
|
||||
name: "too few fields",
|
||||
spec: "* * *",
|
||||
expectedError: "expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||
},
|
||||
{
|
||||
name: "too many fields",
|
||||
spec: "* * * * * *",
|
||||
expectedError: "expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||
},
|
||||
{
|
||||
name: "invalid timezone",
|
||||
spec: "CRON_TZ=Invalid/Zone 0 0 * * *",
|
||||
expectedError: "parse schedule: provided bad location Invalid/Zone: unknown time zone Invalid/Zone",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sched, err := cron.Standard(tc.spec)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, sched)
|
||||
assert.Equal(t, tc.expectedError, err.Error())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("before noon returns noon same day", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sched, err := cron.Standard("0 12 * * *")
|
||||
require.NoError(t, err)
|
||||
at := time.Date(2022, 4, 1, 10, 0, 0, 0, time.UTC)
|
||||
expectedNext := time.Date(2022, 4, 1, 12, 0, 0, 0, time.UTC)
|
||||
assert.Equal(t, expectedNext, sched.Next(at))
|
||||
})
|
||||
|
||||
t.Run("after noon returns noon next day", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sched, err := cron.Standard("0 12 * * *")
|
||||
require.NoError(t, err)
|
||||
at := time.Date(2022, 4, 1, 13, 0, 0, 0, time.UTC)
|
||||
expectedNext := time.Date(2022, 4, 2, 12, 0, 0, 0, time.UTC)
|
||||
assert.Equal(t, expectedNext, sched.Next(at))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ const (
|
||||
APIKeyScopeAuditLogAll APIKeyScope = "audit_log:*"
|
||||
APIKeyScopeAuditLogCreate APIKeyScope = "audit_log:create"
|
||||
APIKeyScopeAuditLogRead APIKeyScope = "audit_log:read"
|
||||
APIKeyScopeAutomationAll APIKeyScope = "automation:*"
|
||||
APIKeyScopeAutomationCreate APIKeyScope = "automation:create"
|
||||
APIKeyScopeAutomationDelete APIKeyScope = "automation:delete"
|
||||
APIKeyScopeAutomationRead APIKeyScope = "automation:read"
|
||||
APIKeyScopeAutomationUpdate APIKeyScope = "automation:update"
|
||||
APIKeyScopeBoundaryUsageAll APIKeyScope = "boundary_usage:*"
|
||||
APIKeyScopeBoundaryUsageDelete APIKeyScope = "boundary_usage:delete"
|
||||
APIKeyScopeBoundaryUsageRead APIKeyScope = "boundary_usage:read"
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AutomationStatus represents the state of an automation.
|
||||
type AutomationStatus string
|
||||
|
||||
const (
|
||||
AutomationStatusDisabled AutomationStatus = "disabled"
|
||||
AutomationStatusPreview AutomationStatus = "preview"
|
||||
AutomationStatusActive AutomationStatus = "active"
|
||||
)
|
||||
|
||||
// Automation represents an automation that bridges external triggers to
|
||||
// Coder chats.
|
||||
type Automation struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Instructions string `json:"instructions"`
|
||||
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
|
||||
MCPServerIDs []uuid.UUID `json:"mcp_server_ids" format:"uuid"`
|
||||
AllowedTools []string `json:"allowed_tools"`
|
||||
Status AutomationStatus `json:"status"`
|
||||
MaxChatCreatesPerHour int32 `json:"max_chat_creates_per_hour"`
|
||||
MaxMessagesPerHour int32 `json:"max_messages_per_hour"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
// CreateAutomationRequest is the request body for creating an automation.
|
||||
type CreateAutomationRequest struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
|
||||
MCPServerIDs []uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"`
|
||||
AllowedTools []string `json:"allowed_tools,omitempty"`
|
||||
MaxChatCreatesPerHour *int32 `json:"max_chat_creates_per_hour,omitempty"`
|
||||
MaxMessagesPerHour *int32 `json:"max_messages_per_hour,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAutomationRequest is the request body for updating an automation.
|
||||
type UpdateAutomationRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Instructions *string `json:"instructions,omitempty"`
|
||||
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
|
||||
MCPServerIDs *[]uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"`
|
||||
AllowedTools *[]string `json:"allowed_tools,omitempty"`
|
||||
Status *AutomationStatus `json:"status,omitempty"`
|
||||
MaxChatCreatesPerHour *int32 `json:"max_chat_creates_per_hour,omitempty"`
|
||||
MaxMessagesPerHour *int32 `json:"max_messages_per_hour,omitempty"`
|
||||
}
|
||||
|
||||
// AutomationTriggerType represents the type of trigger.
|
||||
type AutomationTriggerType string
|
||||
|
||||
const (
|
||||
AutomationTriggerTypeWebhook AutomationTriggerType = "webhook"
|
||||
AutomationTriggerTypeCron AutomationTriggerType = "cron"
|
||||
)
|
||||
|
||||
// AutomationTrigger represents a trigger attached to an automation.
|
||||
type AutomationTrigger struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
AutomationID uuid.UUID `json:"automation_id" format:"uuid"`
|
||||
Type AutomationTriggerType `json:"type"`
|
||||
WebhookURL string `json:"webhook_url,omitempty"`
|
||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||
CronSchedule *string `json:"cron_schedule,omitempty"`
|
||||
Filter json.RawMessage `json:"filter"`
|
||||
LabelPaths json.RawMessage `json:"label_paths"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
// CreateAutomationTriggerRequest is the request body for creating a
|
||||
// trigger.
|
||||
type CreateAutomationTriggerRequest struct {
|
||||
Type AutomationTriggerType `json:"type"`
|
||||
CronSchedule *string `json:"cron_schedule,omitempty"`
|
||||
Filter json.RawMessage `json:"filter,omitempty"`
|
||||
LabelPaths json.RawMessage `json:"label_paths,omitempty"`
|
||||
}
|
||||
|
||||
// AutomationEventStatus represents the outcome of an automation event.
|
||||
type AutomationEventStatus string
|
||||
|
||||
const (
|
||||
AutomationEventStatusFiltered AutomationEventStatus = "filtered"
|
||||
AutomationEventStatusPreview AutomationEventStatus = "preview"
|
||||
AutomationEventStatusCreated AutomationEventStatus = "created"
|
||||
AutomationEventStatusContinued AutomationEventStatus = "continued"
|
||||
AutomationEventStatusRateLimited AutomationEventStatus = "rate_limited"
|
||||
AutomationEventStatusError AutomationEventStatus = "error"
|
||||
)
|
||||
|
||||
// AutomationEvent records the outcome of a single automation event.
|
||||
type AutomationEvent struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
AutomationID uuid.UUID `json:"automation_id" format:"uuid"`
|
||||
TriggerID *uuid.UUID `json:"trigger_id,omitempty" format:"uuid"`
|
||||
ReceivedAt time.Time `json:"received_at" format:"date-time"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
FilterMatched bool `json:"filter_matched"`
|
||||
ResolvedLabels json.RawMessage `json:"resolved_labels"`
|
||||
MatchedChatID *uuid.UUID `json:"matched_chat_id,omitempty" format:"uuid"`
|
||||
CreatedChatID *uuid.UUID `json:"created_chat_id,omitempty" format:"uuid"`
|
||||
Status AutomationEventStatus `json:"status"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TestAutomationRequest is the request body for testing an
|
||||
// automation's filter and session resolution logic.
|
||||
type TestAutomationRequest struct {
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
Filter json.RawMessage `json:"filter,omitempty"`
|
||||
LabelPaths json.RawMessage `json:"label_paths,omitempty"`
|
||||
}
|
||||
|
||||
// AutomationTestResult is the result of a dry-run test against an
|
||||
// automation trigger's filter and session resolution logic.
|
||||
type AutomationTestResult struct {
|
||||
FilterMatched bool `json:"filter_matched"`
|
||||
ResolvedLabels json.RawMessage `json:"resolved_labels"`
|
||||
ExistingChatID *uuid.UUID `json:"existing_chat_id,omitempty" format:"uuid"`
|
||||
WouldCreateChat bool `json:"would_create_new_chat"`
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) CreateAutomation(ctx context.Context, req CreateAutomationRequest) (Automation, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/automations", req)
|
||||
if err != nil {
|
||||
return Automation{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return Automation{}, ReadBodyAsError(res)
|
||||
}
|
||||
var automation Automation
|
||||
return automation, json.NewDecoder(res.Body).Decode(&automation)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) GetAutomation(ctx context.Context, id uuid.UUID) (Automation, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/automations/%s", id), nil)
|
||||
if err != nil {
|
||||
return Automation{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return Automation{}, ReadBodyAsError(res)
|
||||
}
|
||||
var automation Automation
|
||||
return automation, json.NewDecoder(res.Body).Decode(&automation)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) ListAutomations(ctx context.Context) ([]Automation, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/automations", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var automations []Automation
|
||||
return automations, json.NewDecoder(res.Body).Decode(&automations)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) UpdateAutomation(ctx context.Context, id uuid.UUID, req UpdateAutomationRequest) (Automation, error) {
|
||||
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/automations/%s", id), req)
|
||||
if err != nil {
|
||||
return Automation{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return Automation{}, ReadBodyAsError(res)
|
||||
}
|
||||
var automation Automation
|
||||
return automation, json.NewDecoder(res.Body).Decode(&automation)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) DeleteAutomation(ctx context.Context, id uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/automations/%s", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) ListAutomationEvents(ctx context.Context, id uuid.UUID) ([]AutomationEvent, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/automations/%s/events", id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var events []AutomationEvent
|
||||
return events, json.NewDecoder(res.Body).Decode(&events)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) TestAutomation(ctx context.Context, id uuid.UUID, req TestAutomationRequest) (AutomationTestResult, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/automations/%s/test", id), req)
|
||||
if err != nil {
|
||||
return AutomationTestResult{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return AutomationTestResult{}, ReadBodyAsError(res)
|
||||
}
|
||||
var result AutomationTestResult
|
||||
return result, json.NewDecoder(res.Body).Decode(&result)
|
||||
}
|
||||
|
||||
// CreateAutomationTrigger creates a new trigger for an automation.
|
||||
func (c *ExperimentalClient) CreateAutomationTrigger(ctx context.Context, automationID uuid.UUID, req CreateAutomationTriggerRequest) (AutomationTrigger, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/automations/%s/triggers", automationID), req)
|
||||
if err != nil {
|
||||
return AutomationTrigger{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return AutomationTrigger{}, ReadBodyAsError(res)
|
||||
}
|
||||
var trigger AutomationTrigger
|
||||
return trigger, json.NewDecoder(res.Body).Decode(&trigger)
|
||||
}
|
||||
|
||||
// ListAutomationTriggers lists all triggers for an automation.
|
||||
func (c *ExperimentalClient) ListAutomationTriggers(ctx context.Context, automationID uuid.UUID) ([]AutomationTrigger, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/automations/%s/triggers", automationID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var triggers []AutomationTrigger
|
||||
return triggers, json.NewDecoder(res.Body).Decode(&triggers)
|
||||
}
|
||||
|
||||
// DeleteAutomationTrigger deletes a trigger from an automation.
|
||||
func (c *ExperimentalClient) DeleteAutomationTrigger(ctx context.Context, automationID uuid.UUID, triggerID uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/automations/%s/triggers/%s", automationID, triggerID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegenerateAutomationTriggerSecret regenerates the webhook secret for
|
||||
// a trigger.
|
||||
func (c *ExperimentalClient) RegenerateAutomationTriggerSecret(ctx context.Context, automationID uuid.UUID, triggerID uuid.UUID) (AutomationTrigger, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/automations/%s/triggers/%s/regenerate-secret", automationID, triggerID), nil)
|
||||
if err != nil {
|
||||
return AutomationTrigger{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return AutomationTrigger{}, ReadBodyAsError(res)
|
||||
}
|
||||
var trigger AutomationTrigger
|
||||
return trigger, json.NewDecoder(res.Body).Decode(&trigger)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
ResourceAssignOrgRole RBACResource = "assign_org_role"
|
||||
ResourceAssignRole RBACResource = "assign_role"
|
||||
ResourceAuditLog RBACResource = "audit_log"
|
||||
ResourceAutomation RBACResource = "automation"
|
||||
ResourceBoundaryUsage RBACResource = "boundary_usage"
|
||||
ResourceChat RBACResource = "chat"
|
||||
ResourceConnectionLog RBACResource = "connection_log"
|
||||
@@ -82,6 +83,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate},
|
||||
ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign},
|
||||
ResourceAuditLog: {ActionCreate, ActionRead},
|
||||
ResourceAutomation: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceBoundaryUsage: {ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceChat: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceConnectionLog: {ActionRead, ActionUpdate},
|
||||
|
||||
Generated
+20
-20
@@ -191,10 +191,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -324,10 +324,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -457,10 +457,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -552,10 +552,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -955,9 +955,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
Generated
+6
-6
@@ -995,9 +995,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`, `automation:*`, `automation:create`, `automation:delete`, `automation:read`, `automation:update`, `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` |
|
||||
|
||||
## codersdk.AddLicenseRequest
|
||||
|
||||
@@ -7780,9 +7780,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`, `automation`, `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` |
|
||||
|
||||
## codersdk.RateLimitConfig
|
||||
|
||||
|
||||
Generated
+5
-5
@@ -846,11 +846,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`, `automation`, `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` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ export const RBACResourceActions: Partial<
|
||||
create: "create new audit log entries",
|
||||
read: "read audit logs",
|
||||
},
|
||||
automation: {
|
||||
create: "create an automation",
|
||||
delete: "delete an automation",
|
||||
read: "read automation configuration",
|
||||
update: "update an automation",
|
||||
},
|
||||
boundary_usage: {
|
||||
delete: "delete boundary usage statistics",
|
||||
read: "read boundary usage statistics",
|
||||
|
||||
Generated
+171
@@ -225,6 +225,11 @@ export type APIKeyScope =
|
||||
| "audit_log:*"
|
||||
| "audit_log:create"
|
||||
| "audit_log:read"
|
||||
| "automation:*"
|
||||
| "automation:create"
|
||||
| "automation:delete"
|
||||
| "automation:read"
|
||||
| "automation:update"
|
||||
| "boundary_usage:*"
|
||||
| "boundary_usage:delete"
|
||||
| "boundary_usage:read"
|
||||
@@ -434,6 +439,11 @@ export const APIKeyScopes: APIKeyScope[] = [
|
||||
"audit_log:*",
|
||||
"audit_log:create",
|
||||
"audit_log:read",
|
||||
"automation:*",
|
||||
"automation:create",
|
||||
"automation:delete",
|
||||
"automation:read",
|
||||
"automation:update",
|
||||
"boundary_usage:*",
|
||||
"boundary_usage:delete",
|
||||
"boundary_usage:read",
|
||||
@@ -922,6 +932,110 @@ export type AutomaticUpdates = "always" | "never";
|
||||
|
||||
export const AutomaticUpdateses: AutomaticUpdates[] = ["always", "never"];
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* Automation represents an automation that bridges external triggers to
|
||||
* Coder chats.
|
||||
*/
|
||||
export interface Automation {
|
||||
readonly id: string;
|
||||
readonly owner_id: string;
|
||||
readonly organization_id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly instructions: string;
|
||||
readonly model_config_id?: string;
|
||||
readonly mcp_server_ids: readonly string[];
|
||||
readonly allowed_tools: readonly string[];
|
||||
readonly status: AutomationStatus;
|
||||
readonly max_chat_creates_per_hour: number;
|
||||
readonly max_messages_per_hour: number;
|
||||
readonly created_at: string;
|
||||
readonly updated_at: string;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* AutomationEvent records the outcome of a single automation event.
|
||||
*/
|
||||
export interface AutomationEvent {
|
||||
readonly id: string;
|
||||
readonly automation_id: string;
|
||||
readonly trigger_id?: string;
|
||||
readonly received_at: string;
|
||||
readonly payload: Record<string, string>;
|
||||
readonly filter_matched: boolean;
|
||||
readonly resolved_labels: Record<string, string>;
|
||||
readonly matched_chat_id?: string;
|
||||
readonly created_chat_id?: string;
|
||||
readonly status: AutomationEventStatus;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
export type AutomationEventStatus =
|
||||
| "continued"
|
||||
| "created"
|
||||
| "error"
|
||||
| "filtered"
|
||||
| "preview"
|
||||
| "rate_limited";
|
||||
|
||||
export const AutomationEventStatuses: AutomationEventStatus[] = [
|
||||
"continued",
|
||||
"created",
|
||||
"error",
|
||||
"filtered",
|
||||
"preview",
|
||||
"rate_limited",
|
||||
];
|
||||
|
||||
// From codersdk/automations.go
|
||||
export type AutomationStatus = "active" | "disabled" | "preview";
|
||||
|
||||
export const AutomationStatuses: AutomationStatus[] = [
|
||||
"active",
|
||||
"disabled",
|
||||
"preview",
|
||||
];
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* AutomationTestResult is the result of a dry-run test against an
|
||||
* automation trigger's filter and session resolution logic.
|
||||
*/
|
||||
export interface AutomationTestResult {
|
||||
readonly filter_matched: boolean;
|
||||
readonly resolved_labels: Record<string, string>;
|
||||
readonly existing_chat_id?: string;
|
||||
readonly would_create_new_chat: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* AutomationTrigger represents a trigger attached to an automation.
|
||||
*/
|
||||
export interface AutomationTrigger {
|
||||
readonly id: string;
|
||||
readonly automation_id: string;
|
||||
readonly type: AutomationTriggerType;
|
||||
readonly webhook_url?: string;
|
||||
readonly webhook_secret?: string;
|
||||
readonly cron_schedule?: string;
|
||||
readonly filter: Record<string, string>;
|
||||
readonly label_paths: Record<string, string>;
|
||||
readonly created_at: string;
|
||||
readonly updated_at: string;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
export type AutomationTriggerType = "cron" | "webhook";
|
||||
|
||||
export const AutomationTriggerTypes: AutomationTriggerType[] = [
|
||||
"cron",
|
||||
"webhook",
|
||||
];
|
||||
|
||||
// From codersdk/deployment.go
|
||||
/**
|
||||
* AvailableExperiments is an expandable type that returns all safe experiments
|
||||
@@ -2190,6 +2304,34 @@ export interface ConvertLoginRequest {
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* CreateAutomationRequest is the request body for creating an automation.
|
||||
*/
|
||||
export interface CreateAutomationRequest {
|
||||
readonly organization_id: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly instructions?: string;
|
||||
readonly model_config_id?: string;
|
||||
readonly mcp_server_ids?: readonly string[];
|
||||
readonly allowed_tools?: readonly string[];
|
||||
readonly max_chat_creates_per_hour?: number;
|
||||
readonly max_messages_per_hour?: number;
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* CreateAutomationTriggerRequest is the request body for creating a
|
||||
* trigger.
|
||||
*/
|
||||
export interface CreateAutomationTriggerRequest {
|
||||
readonly type: AutomationTriggerType;
|
||||
readonly cron_schedule?: string;
|
||||
readonly filter?: Record<string, string>;
|
||||
readonly label_paths?: Record<string, string>;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* CreateChatMessageRequest is the request to add a message to a chat.
|
||||
@@ -5442,6 +5584,7 @@ export type RBACResource =
|
||||
| "assign_org_role"
|
||||
| "assign_role"
|
||||
| "audit_log"
|
||||
| "automation"
|
||||
| "boundary_usage"
|
||||
| "chat"
|
||||
| "connection_log"
|
||||
@@ -5488,6 +5631,7 @@ export const RBACResources: RBACResource[] = [
|
||||
"assign_org_role",
|
||||
"assign_role",
|
||||
"audit_log",
|
||||
"automation",
|
||||
"boundary_usage",
|
||||
"chat",
|
||||
"connection_log",
|
||||
@@ -6907,6 +7051,17 @@ export const TerminalFontNames: TerminalFontName[] = [
|
||||
"",
|
||||
];
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* TestAutomationRequest is the request body for testing an
|
||||
* automation's filter and session resolution logic.
|
||||
*/
|
||||
export interface TestAutomationRequest {
|
||||
readonly payload: Record<string, string>;
|
||||
readonly filter?: Record<string, string>;
|
||||
readonly label_paths?: Record<string, string>;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export type TimingStage =
|
||||
| "apply"
|
||||
@@ -6970,6 +7125,22 @@ export interface UpdateAppearanceConfig {
|
||||
readonly announcement_banners: readonly BannerConfig[];
|
||||
}
|
||||
|
||||
// From codersdk/automations.go
|
||||
/**
|
||||
* UpdateAutomationRequest is the request body for updating an automation.
|
||||
*/
|
||||
export interface UpdateAutomationRequest {
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
readonly instructions?: string;
|
||||
readonly model_config_id?: string;
|
||||
readonly mcp_server_ids?: string[];
|
||||
readonly allowed_tools?: string[];
|
||||
readonly status?: AutomationStatus;
|
||||
readonly max_chat_creates_per_hour?: number;
|
||||
readonly max_messages_per_hour?: number;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* UpdateChatDesktopEnabledRequest is the request to update the desktop setting.
|
||||
|
||||
Reference in New Issue
Block a user