Compare commits

...

9 Commits

Author SHA1 Message Date
Kyle Carberry 459ea75c57 feat(coderd/automations): implement active mode — chat creation and rate limiting
Adds the Fire() executor that handles the full automation trigger
lifecycle for both webhook and cron paths:

- Rate limiting via CountAutomationChatCreatesInWindow and
  CountAutomationMessagesInWindow (both queries already existed
  but were never called)
- Label-based chat lookup to continue existing conversations
- New chat creation through chatd.Server via ChatCreator interface
- Event recording with matched_chat_id / created_chat_id
- Preview mode still only logs events without acting

Changes:
- automations/executor.go: shared Fire() function with full flow
- automations/chatadapter.go: ChatdAdapter bridges ChatCreator to
  chatd.Server
- exp_automations.go: webhook handler now calls Fire() instead of
  stub event insertion
- cronscheduler: accepts ChatCreator, delegates to Fire()
- coderd.go: ChatDaemon() public getter for chatd.Server
- GetActiveCronTriggers query expanded with rate limit and model
  fields
2026-03-26 14:01:20 +00:00
Kyle Carberry c04f4f6abf feat(coderd/automations): add cron trigger scheduler
Adds a background scheduler that evaluates cron-based automation
triggers every minute. Follows the dbpurge pattern with quartz.Clock
for testability and advisory locking for single-replica execution.

Changes:
- Migration 000453: adds last_triggered_at to automation_triggers
- SQL queries: GetActiveCronTriggers (JOIN with automations),
  UpdateAutomationTriggerLastTriggeredAt
- cron.Standard(): full 5-field cron parser without Weekly/Daily
  restrictions
- cronscheduler package: background goroutine, advisory lock,
  schedule evaluation, event creation, preview/active mode
- Handler validation: cron_schedule validated on trigger creation
- Wired into cli/server.go alongside dbpurge and autobuild
- 20 new tests (13 cron parser + 7 scheduler with quartz mock)
2026-03-26 13:45:05 +00:00
Kyle Carberry b117acfaaf fix(coderd): round-3 review fixes — formatting, validation, cleanup
H1: Regenerate typesGenerated.ts to match SDK changes
  (organization_id, webhook_secret, TestAutomationRequest).

H2: Fix gofmt alignment in AutomationTrigger struct.

M1: Fix safePayload data loss — truncate inner body before
  wrapping in JSON envelope, not after. Prevents complete data
  loss when non-JSON payloads are close to the 64KB limit.

M2: Validate filter (must be JSON object) and label_paths
  (must be map[string]string) in postAutomationTrigger. Prevents
  silent webhook filtering from malformed trigger config.

M3: Replace inline anonymous struct in testAutomation with
  codersdk.TestAutomationRequest to prevent type drift.

M4: Remove unused accessURL parameter from db2sdk.Automation()
  and all 4 call sites.

M5: Add format:"uuid" tags to MCPServerIDs on Automation,
  CreateAutomationRequest, and UpdateAutomationRequest.

L2: Catch FK violations in postAutomation — return 400 with
  descriptive message instead of generic 500.
2026-03-25 23:20:27 +00:00
Kyle Carberry 8d6143bbef fix(coderd): require explicit org, gate webhook, expose secret, handle non-JSON
M1: Add experiment gate in postAutomationWebhook — checks
api.Experiments.Enabled(ExperimentAgents) and returns 200 early
if disabled. Done in-handler (not middleware) to preserve the
always-200 contract.

M2: Replace nondeterministic orgs[0] selection with explicit
OrganizationID field on CreateAutomationRequest. Returns 400
if not provided. Follows the established pattern from chats.

M7: Add WebhookSecret field (omitempty) to AutomationTrigger
SDK type. Populated only in postAutomationTrigger and
regenerateAutomationTriggerSecret responses so the user can
configure their webhook provider. Never returned from list/get.

L2: Add safePayload() that wraps non-JSON webhook bodies in a
valid JSON envelope before storing. Preserves the audit trail
when webhook providers send form-encoded or XML payloads.
2026-03-25 21:12:03 +00:00
Kyle Carberry c6f384cc94 fix(coderd): address round-2 review issues in automations
High fixes:
- MatchFilter: use reflect.DeepEqual instead of != to prevent
  runtime panics on array/object filter values. Add test cases
  for booleans, arrays, and nested objects.
- postAutomation/patchAutomation: handle unique constraint
  violations with 409 Conflict instead of leaking raw Postgres
  errors as 500s. Uses database.IsUniqueViolation pattern.

Medium fixes:
- regenerateAutomationTriggerSecret: reject non-webhook triggers
  with 400 instead of silently setting an unused secret.
- coderd.go: fix mangled indentation on automations, chats, webhook,
  and deployment route blocks. Split r.Use(apiKeyMiddleware) onto
  its own line in /deployment route.
- Add session_test.go with 8 test cases for ResolveLabels covering
  nil/empty input, path extraction, missing paths, and type coercion.

Low fixes:
- testAutomation: return 400 on malformed label_paths instead of
  silently swallowing the parse error.
- Validate rate limit fields (1-1000 range) in both create and
  update handlers to prevent DB check constraint 500s.
2026-03-25 20:36:56 +00:00
Kyle Carberry 17e73f1711 fix(coderd): address critical and high review issues in automations
Critical fixes:
- postAutomationWebhook: parse trigger UUID manually to always return
  200 (never leak 400 to webhook sources)
- deleteAutomationTrigger/regenerateAutomationTriggerSecret: verify
  trigger.AutomationID matches automation from middleware (prevents
  cross-user trigger manipulation)
- truncatePayload: return valid JSON stub instead of byte-slicing
  (which produced broken JSON at 64KB boundary)
- InsertAutomationEvent: scope RBAC check to specific automation
  instead of bare resource type
- Add webhook_secret_key_id to trigger insert/update SQL queries
  (enables dbcrypt key tracking)

High fixes:
- Add index on chats.automation_id for ON DELETE SET NULL performance
- Add unique constraint on automations(owner_id, org_id, name)
- Add index on automation_events(received_at) for purge query
- patchAutomation: validate status against allowed values, reject
  empty name
- postAutomationWebhook: verify trigger.Type is 'webhook' before
  processing
- DeleteAutomationTriggerByID: use ActionUpdate (not ActionDelete)
  on parent automation, consistent with child-entity RBAC pattern
- TestAutomation SDK client: send proper struct with payload/filter/
  label_paths instead of raw JSON
- Remove dead UpdateAutomationTriggerRequest type
- postAutomationTrigger: validate trigger type against known values
- CountAutomationMessagesInWindow: count both 'created' and
  'continued' events toward message rate limit
- Scope CountAutomationChatCreatesInWindow and
  CountAutomationMessagesInWindow RBAC to specific automation
2026-03-25 20:17:36 +00:00
Kyle Carberry 39a5af04bf refactor: extract automation triggers into separate table
Trigger-specific fields (webhook_secret, cron_schedule, filter,
label_paths) move from the automations table into a new
automation_triggers table. An automation can have multiple triggers,
each with its own type (webhook or cron), secret, and filter config.

Each webhook trigger gets its own URL and HMAC secret, so external
systems configure per-trigger webhooks that can be independently
revoked.

The event log table is renamed from automation_webhook_events to
automation_events (events can now come from cron triggers too) and
gains a trigger_id FK to trace which trigger fired.

New CRUD endpoints for triggers nested under
/api/experimental/automations/{automation}/triggers. The webhook
ingestion endpoint moves to /api/v2/automations/triggers/{trigger_id}/webhook.
2026-03-25 19:46:04 +00:00
Kyle Carberry 13f77b2b27 refactor: address review feedback on automations
- Move convertAutomation/convertWebhookEvent to db2sdk package
- Rename session_labels → label_paths (clearer: maps label keys to gjson paths)
- Rename system_prompt → instructions (sent as user message, not system prompt)
- Remove workspace_id column (chat creation handles workspace selection)
- Add cron_schedule column for scheduled automations (nullable, mutually
  exclusive with webhook_secret in v1)
- Make webhook_secret nullable (cron automations don't need it)
2026-03-25 19:22:18 +00:00
Kyle Carberry 13ab3ad058 feat: add automations backend — webhooks to chat bridge
Adds the backend infrastructure for automations, a user-owned resource
that bridges external webhooks to Coder chats. When a webhook arrives,
the system verifies the HMAC-SHA256 signature, evaluates a gjson-based
filter, resolves a chat session via labels, and logs the event.

Database: migration 000452 adds automations table, automation_webhook_events
table, and chats.automation_id FK. RBAC: new "automation" resource type
with CRUD actions. Routes: authenticated CRUD under /api/experimental/automations
(experiment-gated), unauthenticated webhook at /api/v2/automations/{id}/webhook.

Active-mode chat creation through chatd.Server is deferred to a follow-up.
Audit logging requires a resource_type enum addition (separate migration).
2026-03-25 18:40:44 +00:00
53 changed files with 4803 additions and 46 deletions
+6 -2
View File
@@ -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
+12
View File
@@ -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",
+12
View File
@@ -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",
+56
View File
@@ -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())
})
}
+288
View File
@@ -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)
}
+34
View File
@@ -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
}
+48
View File
@@ -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)
})
}
}
+23
View File
@@ -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
}
+79
View File
@@ -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)
})
}
}
+33
View File
@@ -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)
}
+48
View File
@@ -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)
})
}
}
+36
View File
@@ -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 {
+5
View File
@@ -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
+77
View File
@@ -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
}
+192
View File
@@ -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)
}
+119
View File
@@ -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{})
+152 -1
View 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
}
+281
View File
@@ -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()
+106 -1
View File
@@ -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
+1
View File
@@ -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.';
+7
View File
@@ -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)
}
+63
View File
@@ -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
}
+51
View File
@@ -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 {
+21
View File
@@ -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
View File
@@ -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';
+69
View File
@@ -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;
+1
View File
@@ -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
+4
View File
@@ -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));
+723
View File
@@ -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),
))
}
+52
View File
@@ -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))
})
}
}
+11
View File
@@ -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,
+10
View File
@@ -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,
+12
View File
@@ -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,
+23
View File
@@ -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)
+137
View File
@@ -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))
})
})
}
+5
View File
@@ -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"
+286
View File
@@ -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)
}
+2
View File
@@ -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},
+20 -20
View File
@@ -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).
+6 -6
View File
@@ -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
+5 -5
View File
@@ -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).
+6
View File
@@ -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",
+171
View File
@@ -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.