feat: add ai_providers table, queries, dbauthz, audit, RBAC (#24892)

This commit is contained in:
Danny Kopping
2026-05-14 16:10:46 +02:00
committed by GitHub
parent acf57b3b35
commit 841b777ccd
43 changed files with 1960 additions and 232 deletions
+10 -10
View File
@@ -856,11 +856,11 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
)
}
aibridgeProviders, err := ReadAIBridgeProvidersFromEnv(logger, os.Environ())
aiProviders, err := ReadAIProvidersFromEnv(logger, os.Environ())
if err != nil {
return xerrors.Errorf("read aibridge providers from env: %w", err)
return xerrors.Errorf("read AI providers from env: %w", err)
}
vals.AI.BridgeConfig.Providers = append(vals.AI.BridgeConfig.Providers, aibridgeProviders...)
vals.AI.BridgeConfig.Providers = append(vals.AI.BridgeConfig.Providers, aiProviders...)
// Manage push notifications.
webpusher, err := webpush.New(ctx, ptr.Ref(options.Logger.Named("webpush")), options.Database, options.AccessURL.String())
@@ -2926,10 +2926,10 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
return providers, nil
}
// ReadAIBridgeProvidersFromEnv parses CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
// environment variables into a slice of AIBridgeProviderConfig.
// ReadAIProvidersFromEnv parses CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
// environment variables into a slice of AIProviderConfig.
// This follows the same indexed pattern as ReadExternalAuthProvidersFromEnv.
func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AIBridgeProviderConfig, error) {
func ReadAIProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AIProviderConfig, error) {
parsed := serpent.ParseEnviron(environ, "CODER_AIBRIDGE_PROVIDER_")
// Sort by numeric index so that PROVIDER_2 comes before PROVIDER_10.
@@ -2942,7 +2942,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
return strings.Compare(a.Name, b.Name)
})
var providers []codersdk.AIBridgeProviderConfig
var providers []codersdk.AIProviderConfig
for _, v := range parsed {
tokens := strings.SplitN(v.Name, "_", 2)
if len(tokens) != 2 {
@@ -2954,7 +2954,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
return nil, xerrors.Errorf("parse number: %s", v.Name)
}
var provider codersdk.AIBridgeProviderConfig
var provider codersdk.AIProviderConfig
switch {
case len(providers) < providerNum:
return nil, xerrors.Errorf(
@@ -3014,7 +3014,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
case "BEDROCK_SMALL_FAST_MODEL":
provider.BedrockSmallFastModel = v.Value
default:
logger.Warn(context.Background(), "ignoring unknown aibridge provider field (check for typos)",
logger.Warn(context.Background(), "ignoring unknown AI provider field (check for typos)",
slog.F("env", fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_%s", providerNum, key)),
)
}
@@ -3066,7 +3066,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
return providers, nil
}
func hasBedrockFields(p codersdk.AIBridgeProviderConfig) bool {
func hasBedrockFields(p codersdk.AIProviderConfig) bool {
return p.BedrockBaseURL != "" || p.BedrockRegion != "" ||
len(p.BedrockAccessKeys) > 0 || len(p.BedrockAccessKeySecrets) > 0 ||
p.BedrockModel != "" || p.BedrockSmallFastModel != ""
+20 -20
View File
@@ -14,13 +14,13 @@ import (
"github.com/coder/coder/v2/testutil"
)
func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
func TestReadAIProvidersFromEnv(t *testing.T) {
t.Parallel()
tests := []struct {
name string
env []string
expected []codersdk.AIBridgeProviderConfig
expected []codersdk.AIProviderConfig
errContains string
}{
{
@@ -36,7 +36,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_BASE_URL=https://api.anthropic.com/",
"CODER_AIBRIDGE_PROVIDER_0_DUMP_DIR=/tmp/aibridge-dump",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{
Type: aibridge.ProviderAnthropic,
Name: "anthropic-zdr",
@@ -55,7 +55,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_1_NAME=anthropic-eu",
"CODER_AIBRIDGE_PROVIDER_1_BASE_URL=https://eu.api.anthropic.com/",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: "anthropic-us"},
{Type: aibridge.ProviderAnthropic, Name: "anthropic-eu", BaseURL: "https://eu.api.anthropic.com/"},
},
@@ -65,7 +65,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
env: []string{
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
},
},
@@ -79,7 +79,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_2_NAME=copilot-custom",
"CODER_AIBRIDGE_PROVIDER_2_BASE_URL=https://custom.copilot.com",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: "anthropic-main"},
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
{Type: aibridge.ProviderCopilot, Name: "copilot-custom", BaseURL: "https://custom.copilot.com"},
@@ -97,7 +97,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_SMALL_FAST_MODEL=anthropic.claude-3-haiku",
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_BASE_URL=https://bedrock.us-west-2.amazonaws.com",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{
Type: aibridge.ProviderAnthropic,
Name: "anthropic-bedrock",
@@ -118,7 +118,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
"CODER_AIBRIDGE_PROVIDER_0_NAME=first",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "first"},
{Type: aibridge.ProviderAnthropic, Name: "second"},
},
@@ -172,7 +172,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_KEY=sk-xxx",
"SOME_OTHER_VAR=hello",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-xxx"}},
},
},
@@ -186,7 +186,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEYS=AKID",
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEY_SECRETS=secret",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{
Type: aibridge.ProviderAnthropic,
Name: aibridge.ProviderAnthropic,
@@ -245,7 +245,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
"CODER_AIBRIDGE_PROVIDER_0_KEYS=sk-a,sk-b,sk-c",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-a", "sk-b", "sk-c"}},
},
},
@@ -255,7 +255,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
"CODER_AIBRIDGE_PROVIDER_0_KEYS= sk-a , sk-b ",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-a", "sk-b"}},
},
},
@@ -291,7 +291,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEYS=AKID1,AKID2",
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEY_SECRETS=secret1,secret2",
},
expected: []codersdk.AIBridgeProviderConfig{
expected: []codersdk.AIProviderConfig{
{
Type: aibridge.ProviderAnthropic,
Name: aibridge.ProviderAnthropic,
@@ -324,7 +324,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
providers, err := ReadAIBridgeProvidersFromEnv(slogtest.Make(t, nil), tt.env)
providers, err := ReadAIProvidersFromEnv(slogtest.Make(t, nil), tt.env)
if tt.errContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
@@ -342,20 +342,20 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
// Indices 0, 1, 2, ..., 10 — verifies that 10 sorts after 2,
// not between 1 and 2 as a lexicographic sort would do.
var env []string
var expected []codersdk.AIBridgeProviderConfig
var expected []codersdk.AIProviderConfig
for i := range 11 {
env = append(env,
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_TYPE=openai", i),
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_KEY=sk-%d", i, i),
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_NAME=p%d", i, i),
)
expected = append(expected, codersdk.AIBridgeProviderConfig{
expected = append(expected, codersdk.AIProviderConfig{
Type: aibridge.ProviderOpenAI,
Name: fmt.Sprintf("p%d", i),
Keys: []string{fmt.Sprintf("sk-%d", i)},
})
}
providers, err := ReadAIBridgeProvidersFromEnv(slogtest.Make(t, nil), env)
providers, err := ReadAIProvidersFromEnv(slogtest.Make(t, nil), env)
require.NoError(t, err)
require.Equal(t, expected, providers)
})
@@ -365,17 +365,17 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
// A typo like TPYE instead of TYPE should not prevent startup;
// the function logs a warning and continues.
sink := testutil.NewFakeSink(t)
providers, err := ReadAIBridgeProvidersFromEnv(sink.Logger(), []string{
providers, err := ReadAIProvidersFromEnv(sink.Logger(), []string{
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
"CODER_AIBRIDGE_PROVIDER_0_TPYE=openai",
})
require.NoError(t, err)
require.Equal(t, []codersdk.AIBridgeProviderConfig{
require.Equal(t, []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
}, providers)
warnings := sink.Entries(func(e slog.SinkEntry) bool {
return e.Message == "ignoring unknown aibridge provider field (check for typos)"
return e.Message == "ignoring unknown AI provider field (check for typos)"
})
require.Len(t, warnings, 1)
require.Len(t, warnings[0].Fields, 1)
+2 -2
View File
@@ -872,8 +872,8 @@ aibridgeproxy:
# (default: <unset>, type: string)
key_file: ""
# Deprecated: This value is now derived automatically from the configured AI
# Bridge providers' base URLs. Setting this value has no effect. This option will
# be removed in a future release.
# providers' base URLs. Setting this value has no effect. This option will be
# removed in a future release.
# (default: <unset>, type: string-array)
domain_allowlist: []
# URL of an upstream HTTP proxy to chain tunneled (non-allowlisted) requests
+47 -31
View File
@@ -13889,7 +13889,7 @@ const docTemplate = `{
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
"$ref": "#/definitions/codersdk.AIProviderConfig"
}
},
"rate_limit": {
@@ -14010,36 +14010,6 @@ const docTemplate = `{
}
}
},
"codersdk.AIBridgeProviderConfig": {
"type": "object",
"properties": {
"base_url": {
"description": "BaseURL is the base URL of the upstream provider API.",
"type": "string"
},
"bedrock_model": {
"type": "string"
},
"bedrock_region": {
"type": "string"
},
"bedrock_small_fast_model": {
"type": "string"
},
"dump_dir": {
"description": "DumpDir is the directory path for dumping API requests and responses.",
"type": "string"
},
"name": {
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"type": "string"
}
}
},
"codersdk.AIBridgeProxyConfig": {
"type": "object",
"properties": {
@@ -14421,6 +14391,36 @@ const docTemplate = `{
}
}
},
"codersdk.AIProviderConfig": {
"type": "object",
"properties": {
"base_url": {
"description": "BaseURL is the base URL of the upstream provider API.",
"type": "string"
},
"bedrock_model": {
"type": "string"
},
"bedrock_region": {
"type": "string"
},
"bedrock_small_fast_model": {
"type": "string"
},
"dump_dir": {
"description": "DumpDir is the directory path for dumping API requests and responses.",
"type": "string"
},
"name": {
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"type": "string"
}
}
},
"codersdk.APIAllowListTarget": {
"type": "object",
"properties": {
@@ -14522,6 +14522,11 @@ const docTemplate = `{
"ai_model_price:*",
"ai_model_price:read",
"ai_model_price:update",
"ai_provider:*",
"ai_provider:create",
"ai_provider:delete",
"ai_provider:read",
"ai_provider:update",
"ai_seat:*",
"ai_seat:create",
"ai_seat:read",
@@ -14737,6 +14742,11 @@ const docTemplate = `{
"APIKeyScopeAiModelPriceAll",
"APIKeyScopeAiModelPriceRead",
"APIKeyScopeAiModelPriceUpdate",
"APIKeyScopeAiProviderAll",
"APIKeyScopeAiProviderCreate",
"APIKeyScopeAiProviderDelete",
"APIKeyScopeAiProviderRead",
"APIKeyScopeAiProviderUpdate",
"APIKeyScopeAiSeatAll",
"APIKeyScopeAiSeatCreate",
"APIKeyScopeAiSeatRead",
@@ -21351,6 +21361,7 @@ const docTemplate = `{
"enum": [
"*",
"ai_model_price",
"ai_provider",
"ai_seat",
"aibridge_interception",
"api_key",
@@ -21399,6 +21410,7 @@ const docTemplate = `{
"x-enum-varnames": [
"ResourceWildcard",
"ResourceAiModelPrice",
"ResourceAIProvider",
"ResourceAiSeat",
"ResourceAibridgeInterception",
"ResourceApiKey",
@@ -21658,6 +21670,8 @@ const docTemplate = `{
"workspace_app",
"task",
"ai_seat",
"ai_provider",
"ai_provider_key",
"chat",
"user_secret"
],
@@ -21689,6 +21703,8 @@ const docTemplate = `{
"ResourceTypeWorkspaceApp",
"ResourceTypeTask",
"ResourceTypeAISeat",
"ResourceTypeAIProvider",
"ResourceTypeAIProviderKey",
"ResourceTypeChat",
"ResourceTypeUserSecret"
]
+47 -31
View File
@@ -12361,7 +12361,7 @@
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
"$ref": "#/definitions/codersdk.AIProviderConfig"
}
},
"rate_limit": {
@@ -12482,36 +12482,6 @@
}
}
},
"codersdk.AIBridgeProviderConfig": {
"type": "object",
"properties": {
"base_url": {
"description": "BaseURL is the base URL of the upstream provider API.",
"type": "string"
},
"bedrock_model": {
"type": "string"
},
"bedrock_region": {
"type": "string"
},
"bedrock_small_fast_model": {
"type": "string"
},
"dump_dir": {
"description": "DumpDir is the directory path for dumping API requests and responses.",
"type": "string"
},
"name": {
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"type": "string"
}
}
},
"codersdk.AIBridgeProxyConfig": {
"type": "object",
"properties": {
@@ -12893,6 +12863,36 @@
}
}
},
"codersdk.AIProviderConfig": {
"type": "object",
"properties": {
"base_url": {
"description": "BaseURL is the base URL of the upstream provider API.",
"type": "string"
},
"bedrock_model": {
"type": "string"
},
"bedrock_region": {
"type": "string"
},
"bedrock_small_fast_model": {
"type": "string"
},
"dump_dir": {
"description": "DumpDir is the directory path for dumping API requests and responses.",
"type": "string"
},
"name": {
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"type": "string"
}
}
},
"codersdk.APIAllowListTarget": {
"type": "object",
"properties": {
@@ -12986,6 +12986,11 @@
"ai_model_price:*",
"ai_model_price:read",
"ai_model_price:update",
"ai_provider:*",
"ai_provider:create",
"ai_provider:delete",
"ai_provider:read",
"ai_provider:update",
"ai_seat:*",
"ai_seat:create",
"ai_seat:read",
@@ -13201,6 +13206,11 @@
"APIKeyScopeAiModelPriceAll",
"APIKeyScopeAiModelPriceRead",
"APIKeyScopeAiModelPriceUpdate",
"APIKeyScopeAiProviderAll",
"APIKeyScopeAiProviderCreate",
"APIKeyScopeAiProviderDelete",
"APIKeyScopeAiProviderRead",
"APIKeyScopeAiProviderUpdate",
"APIKeyScopeAiSeatAll",
"APIKeyScopeAiSeatCreate",
"APIKeyScopeAiSeatRead",
@@ -19575,6 +19585,7 @@
"enum": [
"*",
"ai_model_price",
"ai_provider",
"ai_seat",
"aibridge_interception",
"api_key",
@@ -19623,6 +19634,7 @@
"x-enum-varnames": [
"ResourceWildcard",
"ResourceAiModelPrice",
"ResourceAIProvider",
"ResourceAiSeat",
"ResourceAibridgeInterception",
"ResourceApiKey",
@@ -19872,6 +19884,8 @@
"workspace_app",
"task",
"ai_seat",
"ai_provider",
"ai_provider_key",
"chat",
"user_secret"
],
@@ -19903,6 +19917,8 @@
"ResourceTypeWorkspaceApp",
"ResourceTypeTask",
"ResourceTypeAISeat",
"ResourceTypeAIProvider",
"ResourceTypeAIProviderKey",
"ResourceTypeChat",
"ResourceTypeUserSecret"
]
+2
View File
@@ -34,6 +34,8 @@ type Auditable interface {
idpsync.RoleSyncSettings |
database.TaskTable |
database.AiSeatState |
database.AIProvider |
database.AIProviderKey |
database.Chat |
database.UserSecret
}
+21
View File
@@ -134,6 +134,13 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Name
case database.AiSeatState:
return "AI Seat"
case database.AIProvider:
return typed.Name
case database.AIProviderKey:
// Provider keys have no user-facing name; show the parent
// provider's UUID so the row can be correlated back to its
// provider in the audit UI.
return typed.ProviderID.String()
case database.Chat:
// Chat titles can contain sensitive content (secrets, internal
// project names), so we use a short UUID prefix as a display
@@ -210,6 +217,10 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.AiSeatState:
return typed.UserID
case database.AIProvider:
return typed.ID
case database.AIProviderKey:
return typed.ID
case database.Chat:
return typed.ID
case database.UserSecret:
@@ -271,6 +282,10 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeTask
case database.AiSeatState:
return database.ResourceTypeAiSeat
case database.AIProvider:
return database.ResourceTypeAiProvider
case database.AIProviderKey:
return database.ResourceTypeAiProviderKey
case database.Chat:
return database.ResourceTypeChat
case database.UserSecret:
@@ -335,6 +350,12 @@ func ResourceRequiresOrgID[T Auditable]() bool {
return true
case database.AiSeatState:
return false
case database.AIProvider:
// AI providers are deployment-scoped, not org-scoped.
return false
case database.AIProviderKey:
// AI provider keys are deployment-scoped, not org-scoped.
return false
case database.Chat:
// Chats always have a non-null organization_id (since
// migration 000467).
+1
View File
@@ -10,6 +10,7 @@ const (
CheckAiModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
CheckAiModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
CheckAiModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
CheckAiProvidersNameCheck CheckConstraint = "ai_providers_name_check" // ai_providers
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
+102
View File
@@ -1851,6 +1851,20 @@ func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParam
return q.db.CustomRoles(ctx, arg)
}
func (q *querier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
return err
}
return q.db.DeleteAIProviderByID(ctx, id)
}
func (q *querier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
return err
}
return q.db.DeleteAIProviderKey(ctx, id)
}
func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error {
return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id)
}
@@ -2488,6 +2502,52 @@ func (q *querier) GetAIModelPriceByProviderModel(ctx context.Context, arg databa
return q.db.GetAIModelPriceByProviderModel(ctx, arg)
}
func (q *querier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
}
return q.db.GetAIProviderByID(ctx, id)
}
func (q *querier) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
}
return q.db.GetAIProviderByName(ctx, name)
}
func (q *querier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return database.AIProviderKey{}, err
}
return q.db.GetAIProviderKeyByID(ctx, id)
}
func (q *querier) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
// This query intentionally returns every key row, including those
// whose provider has been soft-deleted, so the dbcrypt key rotation
// utility can re-encrypt every row that holds a foreign-key
// reference to dbcrypt_keys.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return nil, err
}
return q.db.GetAIProviderKeys(ctx)
}
func (q *querier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return nil, err
}
return q.db.GetAIProviderKeysByProviderID(ctx, providerID)
}
func (q *querier) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
return nil, err
}
return q.db.GetAIProviders(ctx, arg)
}
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
}
@@ -5193,6 +5253,20 @@ func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.Ins
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
}
func (q *querier) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
}
return q.db.InsertAIProvider(ctx, arg)
}
func (q *querier) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
return database.AIProviderKey{}, err
}
return q.db.InsertAIProviderKey(ctx, arg)
}
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
// TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we
// don't currently have a capability to conditionally deny creating resources by owner ID in a role.
@@ -6259,6 +6333,13 @@ func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params da
return q.db.UpdateAIBridgeInterceptionEnded(ctx, params)
}
func (q *querier) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
}
return q.db.UpdateAIProvider(ctx, arg)
}
func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) {
return q.db.GetAPIKeyByID(ctx, arg.ID)
@@ -6545,6 +6626,27 @@ func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCusto
return q.db.UpdateCustomRole(ctx, arg)
}
func (q *querier) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
// Encrypted columns can be rewritten on any row, including those
// whose provider has been soft-deleted, so the dbcrypt rotation can
// move every FK reference to a new key digest before old keys are
// revoked.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
return database.AIProviderKey{}, err
}
return q.db.UpdateEncryptedAIProviderKey(ctx, arg)
}
func (q *querier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
// Settings can be rewritten on any row, including soft-deleted ones,
// so the dbcrypt rotation can move every FK reference to a new key
// digest before old keys are revoked.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
}
return q.db.UpdateEncryptedAIProviderSettings(ctx, arg)
}
func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
+97
View File
@@ -6208,6 +6208,103 @@ func (s *MethodTestSuite) TestAIBridge() {
db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()).Return(database.AiModelPrice{}, nil).AnyTimes()
check.Args(database.GetAIModelPriceByProviderModelParams{}).Asserts(rbac.ResourceAiModelPrice, policy.ActionRead)
}))
s.Run("GetAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
dbm.EXPECT().GetAIProviderByID(gomock.Any(), provider.ID).Return(provider, nil).AnyTimes()
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
}))
s.Run("GetAIProviderByName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
dbm.EXPECT().GetAIProviderByName(gomock.Any(), provider.Name).Return(provider, nil).AnyTimes()
check.Args(provider.Name).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
}))
s.Run("GetAIProviders", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
providerA := testutil.Fake(s.T(), faker, database.AIProvider{})
providerB := testutil.Fake(s.T(), faker, database.AIProvider{})
arg := database.GetAIProvidersParams{}
dbm.EXPECT().GetAIProviders(gomock.Any(), arg).Return([]database.AIProvider{providerA, providerB}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProvider{providerA, providerB})
}))
s.Run("InsertAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.InsertAIProviderParams{
ID: uuid.New(),
Type: database.AiProviderTypeOpenai,
Name: "test-provider",
Enabled: true,
BaseUrl: "https://api.example.com/",
}
provider := testutil.Fake(s.T(), faker, database.AIProvider{ID: arg.ID, Name: arg.Name})
dbm.EXPECT().InsertAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(provider)
}))
s.Run("UpdateAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
arg := database.UpdateAIProviderParams{
ID: provider.ID,
Enabled: true,
BaseUrl: "https://api.example.com/",
}
dbm.EXPECT().UpdateAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
}))
s.Run("DeleteAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
dbm.EXPECT().DeleteAIProviderByID(gomock.Any(), provider.ID).Return(nil).AnyTimes()
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
}))
s.Run("UpdateEncryptedAIProviderSettings", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
arg := database.UpdateEncryptedAIProviderSettingsParams{
ID: provider.ID,
Settings: sql.NullString{String: "encrypted-settings", Valid: true},
}
dbm.EXPECT().UpdateEncryptedAIProviderSettings(gomock.Any(), arg).Return(provider, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
}))
s.Run("GetAIProviderKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
dbm.EXPECT().GetAIProviderKeyByID(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(key)
}))
s.Run("GetAIProviderKeysByProviderID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
dbm.EXPECT().GetAIProviderKeysByProviderID(gomock.Any(), provider.ID).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
}))
s.Run("GetAIProviderKeys", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{})
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{})
dbm.EXPECT().GetAIProviderKeys(gomock.Any()).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
check.Args().Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
}))
s.Run("InsertAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
arg := database.InsertAIProviderKeyParams{
ID: uuid.New(),
ProviderID: provider.ID,
APIKey: "test-key",
}
key := testutil.Fake(s.T(), faker, database.AIProviderKey{ID: arg.ID, ProviderID: arg.ProviderID})
dbm.EXPECT().InsertAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(key)
}))
s.Run("DeleteAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
dbm.EXPECT().DeleteAIProviderKey(gomock.Any(), key.ID).Return(nil).AnyTimes()
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
}))
s.Run("UpdateEncryptedAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
arg := database.UpdateEncryptedAIProviderKeyParams{
ID: key.ID,
APIKey: "encrypted-api-key",
}
dbm.EXPECT().UpdateEncryptedAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(key)
}))
}
func (s *MethodTestSuite) TestTelemetry() {
+56
View File
@@ -169,6 +169,62 @@ func ChatModelConfig(t testing.TB, db database.Store, seed database.ChatModelCon
return cfg
}
func AIProvider(t testing.TB, db database.Store, seed database.AIProvider, munge ...func(*database.InsertAIProviderParams)) database.AIProvider {
t.Helper()
id := seed.ID
if id == uuid.Nil {
id = uuid.New()
}
provType := seed.Type
if provType == "" {
provType = database.AiProviderTypeOpenai
}
name := takeFirst(seed.Name, testutil.GetRandomNameHyphenated(t))
displayName := seed.DisplayName
if !displayName.Valid {
displayName = sql.NullString{String: name, Valid: true}
}
params := database.InsertAIProviderParams{
ID: id,
Type: provType,
Name: name,
DisplayName: displayName,
Enabled: takeFirst(seed.Enabled, true),
BaseUrl: takeFirst(seed.BaseUrl, "https://api.example.com/"),
Settings: seed.Settings,
SettingsKeyID: seed.SettingsKeyID,
}
for _, fn := range munge {
fn(&params)
}
provider, err := db.InsertAIProvider(genCtx, params)
require.NoError(t, err, "insert ai provider")
return provider
}
func AIProviderKey(t testing.TB, db database.Store, seed database.AIProviderKey, munge ...func(*database.InsertAIProviderKeyParams)) database.AIProviderKey {
t.Helper()
id := seed.ID
if id == uuid.Nil {
id = uuid.New()
}
now := dbtime.Now()
params := database.InsertAIProviderKeyParams{
ID: id,
ProviderID: seed.ProviderID,
APIKey: takeFirst(seed.APIKey, "test-key"),
ApiKeyKeyID: seed.ApiKeyKeyID,
CreatedAt: takeFirst(seed.CreatedAt, now),
UpdatedAt: takeFirst(seed.UpdatedAt, now),
}
for _, fn := range munge {
fn(&params)
}
key, err := db.InsertAIProviderKey(genCtx, params)
require.NoError(t, err, "insert ai provider key")
return key
}
func ChatProvider(t testing.TB, db database.Store, seed database.ChatProvider, munge ...func(*database.InsertChatProviderParams)) database.ChatProvider {
t.Helper()
params := database.InsertChatProviderParams{
+104
View File
@@ -377,6 +377,22 @@ func (m queryMetricsStore) CustomRoles(ctx context.Context, arg database.CustomR
return r0, r1
}
func (m queryMetricsStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteAIProviderByID(ctx, id)
m.queryLatencies.WithLabelValues("DeleteAIProviderByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderByID").Inc()
return r0
}
func (m queryMetricsStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteAIProviderKey(ctx, id)
m.queryLatencies.WithLabelValues("DeleteAIProviderKey").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderKey").Inc()
return r0
}
func (m queryMetricsStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
start := time.Now()
r0 := m.s.DeleteAPIKeyByID(ctx, id)
@@ -985,6 +1001,54 @@ func (m queryMetricsStore) GetAIModelPriceByProviderModel(ctx context.Context, a
return r0, r1
}
func (m queryMetricsStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviderByID(ctx, id)
m.queryLatencies.WithLabelValues("GetAIProviderByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviderByName(ctx, name)
m.queryLatencies.WithLabelValues("GetAIProviderByName").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByName").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviderKeyByID(ctx, id)
m.queryLatencies.WithLabelValues("GetAIProviderKeyByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeyByID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviderKeys(ctx)
m.queryLatencies.WithLabelValues("GetAIProviderKeys").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeys").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviderKeysByProviderID(ctx, providerID)
m.queryLatencies.WithLabelValues("GetAIProviderKeysByProviderID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeysByProviderID").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.GetAIProviders(ctx, arg)
m.queryLatencies.WithLabelValues("GetAIProviders").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviders").Inc()
return r0, r1
}
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
start := time.Now()
r0, r1 := m.s.GetAPIKeyByID(ctx, id)
@@ -3561,6 +3625,22 @@ func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg dat
return r0, r1
}
func (m queryMetricsStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.InsertAIProvider(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIProvider").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProvider").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
start := time.Now()
r0, r1 := m.s.InsertAIProviderKey(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIProviderKey").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProviderKey").Inc()
return r0, r1
}
func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
start := time.Now()
r0, r1 := m.s.InsertAPIKey(ctx, arg)
@@ -4513,6 +4593,14 @@ func (m queryMetricsStore) UpdateAIBridgeInterceptionEnded(ctx context.Context,
return r0, r1
}
func (m queryMetricsStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.UpdateAIProvider(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateAIProvider").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAIProvider").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
start := time.Now()
r0 := m.s.UpdateAPIKeyByID(ctx, arg)
@@ -4697,6 +4785,22 @@ func (m queryMetricsStore) UpdateCustomRole(ctx context.Context, arg database.Up
return r0, r1
}
func (m queryMetricsStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
start := time.Now()
r0, r1 := m.s.UpdateEncryptedAIProviderKey(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderKey").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderKey").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
start := time.Now()
r0, r1 := m.s.UpdateEncryptedAIProviderSettings(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderSettings").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderSettings").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
start := time.Now()
r0, r1 := m.s.UpdateExternalAuthLink(ctx, arg)
+193
View File
@@ -603,6 +603,34 @@ func (mr *MockStoreMockRecorder) CustomRoles(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomRoles", reflect.TypeOf((*MockStore)(nil).CustomRoles), ctx, arg)
}
// DeleteAIProviderByID mocks base method.
func (m *MockStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAIProviderByID", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAIProviderByID indicates an expected call of DeleteAIProviderByID.
func (mr *MockStoreMockRecorder) DeleteAIProviderByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderByID", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderByID), ctx, id)
}
// DeleteAIProviderKey mocks base method.
func (m *MockStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAIProviderKey", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAIProviderKey indicates an expected call of DeleteAIProviderKey.
func (mr *MockStoreMockRecorder) DeleteAIProviderKey(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderKey", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderKey), ctx, id)
}
// DeleteAPIKeyByID mocks base method.
func (m *MockStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
m.ctrl.T.Helper()
@@ -1698,6 +1726,96 @@ func (mr *MockStoreMockRecorder) GetAIModelPriceByProviderModel(ctx, arg any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIModelPriceByProviderModel", reflect.TypeOf((*MockStore)(nil).GetAIModelPriceByProviderModel), ctx, arg)
}
// GetAIProviderByID mocks base method.
func (m *MockStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviderByID", ctx, id)
ret0, _ := ret[0].(database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviderByID indicates an expected call of GetAIProviderByID.
func (mr *MockStoreMockRecorder) GetAIProviderByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderByID), ctx, id)
}
// GetAIProviderByName mocks base method.
func (m *MockStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviderByName", ctx, name)
ret0, _ := ret[0].(database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviderByName indicates an expected call of GetAIProviderByName.
func (mr *MockStoreMockRecorder) GetAIProviderByName(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByName", reflect.TypeOf((*MockStore)(nil).GetAIProviderByName), ctx, name)
}
// GetAIProviderKeyByID mocks base method.
func (m *MockStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviderKeyByID", ctx, id)
ret0, _ := ret[0].(database.AIProviderKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviderKeyByID indicates an expected call of GetAIProviderKeyByID.
func (mr *MockStoreMockRecorder) GetAIProviderKeyByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeyByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeyByID), ctx, id)
}
// GetAIProviderKeys mocks base method.
func (m *MockStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviderKeys", ctx)
ret0, _ := ret[0].([]database.AIProviderKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviderKeys indicates an expected call of GetAIProviderKeys.
func (mr *MockStoreMockRecorder) GetAIProviderKeys(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeys", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeys), ctx)
}
// GetAIProviderKeysByProviderID mocks base method.
func (m *MockStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviderKeysByProviderID", ctx, providerID)
ret0, _ := ret[0].([]database.AIProviderKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviderKeysByProviderID indicates an expected call of GetAIProviderKeysByProviderID.
func (mr *MockStoreMockRecorder) GetAIProviderKeysByProviderID(ctx, providerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeysByProviderID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeysByProviderID), ctx, providerID)
}
// GetAIProviders mocks base method.
func (m *MockStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIProviders", ctx, arg)
ret0, _ := ret[0].([]database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIProviders indicates an expected call of GetAIProviders.
func (mr *MockStoreMockRecorder) GetAIProviders(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviders", reflect.TypeOf((*MockStore)(nil).GetAIProviders), ctx, arg)
}
// GetAPIKeyByID mocks base method.
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
m.ctrl.T.Helper()
@@ -6677,6 +6795,36 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg)
}
// InsertAIProvider mocks base method.
func (m *MockStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIProvider", ctx, arg)
ret0, _ := ret[0].(database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIProvider indicates an expected call of InsertAIProvider.
func (mr *MockStoreMockRecorder) InsertAIProvider(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProvider", reflect.TypeOf((*MockStore)(nil).InsertAIProvider), ctx, arg)
}
// InsertAIProviderKey mocks base method.
func (m *MockStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIProviderKey", ctx, arg)
ret0, _ := ret[0].(database.AIProviderKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIProviderKey indicates an expected call of InsertAIProviderKey.
func (mr *MockStoreMockRecorder) InsertAIProviderKey(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProviderKey", reflect.TypeOf((*MockStore)(nil).InsertAIProviderKey), ctx, arg)
}
// InsertAPIKey mocks base method.
func (m *MockStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
m.ctrl.T.Helper()
@@ -8539,6 +8687,21 @@ func (mr *MockStoreMockRecorder) UpdateAIBridgeInterceptionEnded(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIBridgeInterceptionEnded", reflect.TypeOf((*MockStore)(nil).UpdateAIBridgeInterceptionEnded), ctx, arg)
}
// UpdateAIProvider mocks base method.
func (m *MockStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAIProvider", ctx, arg)
ret0, _ := ret[0].(database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateAIProvider indicates an expected call of UpdateAIProvider.
func (mr *MockStoreMockRecorder) UpdateAIProvider(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIProvider", reflect.TypeOf((*MockStore)(nil).UpdateAIProvider), ctx, arg)
}
// UpdateAPIKeyByID mocks base method.
func (m *MockStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
m.ctrl.T.Helper()
@@ -8881,6 +9044,36 @@ func (mr *MockStoreMockRecorder) UpdateCustomRole(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCustomRole", reflect.TypeOf((*MockStore)(nil).UpdateCustomRole), ctx, arg)
}
// UpdateEncryptedAIProviderKey mocks base method.
func (m *MockStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderKey", ctx, arg)
ret0, _ := ret[0].(database.AIProviderKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateEncryptedAIProviderKey indicates an expected call of UpdateEncryptedAIProviderKey.
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderKey(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderKey", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderKey), ctx, arg)
}
// UpdateEncryptedAIProviderSettings mocks base method.
func (m *MockStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderSettings", ctx, arg)
ret0, _ := ret[0].(database.AIProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateEncryptedAIProviderSettings indicates an expected call of UpdateEncryptedAIProviderSettings.
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderSettings(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderSettings", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderSettings), ctx, arg)
}
// UpdateExternalAuthLink mocks base method.
func (m *MockStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
m.ctrl.T.Helper()
+75 -2
View File
@@ -10,6 +10,11 @@ CREATE TYPE agent_key_scope_enum AS ENUM (
'no_user_data'
);
CREATE TYPE ai_provider_type AS ENUM (
'openai',
'anthropic'
);
CREATE TYPE ai_seat_usage_reason AS ENUM (
'aibridge',
'task'
@@ -226,7 +231,12 @@ CREATE TYPE api_key_scope AS ENUM (
'ai_seat:read',
'ai_model_price:*',
'ai_model_price:read',
'ai_model_price:update'
'ai_model_price:update',
'ai_provider:*',
'ai_provider:create',
'ai_provider:delete',
'ai_provider:read',
'ai_provider:update'
);
CREATE TYPE app_sharing_level AS ENUM (
@@ -533,7 +543,9 @@ CREATE TYPE resource_type AS ENUM (
'task',
'ai_seat',
'chat',
'user_secret'
'user_secret',
'ai_provider',
'ai_provider_key'
);
CREATE TYPE shareable_workspace_owners AS ENUM (
@@ -1108,6 +1120,46 @@ CREATE TABLE ai_model_prices (
COMMENT ON TABLE ai_model_prices IS 'Per-model token prices used by AI Bridge to compute interception cost.';
CREATE TABLE ai_provider_keys (
id uuid DEFAULT gen_random_uuid() NOT NULL,
provider_id uuid NOT NULL,
api_key text NOT NULL,
api_key_key_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
CREATE TABLE ai_providers (
id uuid DEFAULT gen_random_uuid() NOT NULL,
type ai_provider_type NOT NULL,
name text NOT NULL,
display_name text,
enabled boolean DEFAULT true NOT NULL,
deleted boolean DEFAULT false NOT NULL,
base_url text NOT NULL,
settings text,
settings_key_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT ai_providers_name_check CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text))
);
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
CREATE TABLE ai_seat_state (
user_id uuid NOT NULL,
first_used_at timestamp with time zone NOT NULL,
@@ -3409,6 +3461,12 @@ ALTER TABLE ONLY workspace_agent_stats
ALTER TABLE ONLY ai_model_prices
ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
ALTER TABLE ONLY ai_provider_keys
ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ai_providers
ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ai_seat_state
ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
@@ -3775,6 +3833,8 @@ ALTER TABLE ONLY workspace_resources
ALTER TABLE ONLY workspaces
ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
CREATE INDEX api_keys_last_used_idx ON api_keys USING btree (last_used DESC);
COMMENT ON INDEX api_keys_last_used_idx IS 'Index for optimizing api_keys queries filtering by last_used';
@@ -3783,6 +3843,10 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys USING btree (provider_id);
CREATE INDEX idx_ai_providers_enabled ON ai_providers USING btree (enabled) WHERE (deleted = false);
CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions USING btree (client);
CREATE INDEX idx_aibridge_interceptions_client_session_id ON aibridge_interceptions USING btree (client_session_id) WHERE (client_session_id IS NOT NULL);
@@ -4149,6 +4213,15 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'U
the uniqueness requirement. A trigger allows us to enforce uniqueness going
forward without requiring a migration to clean up historical data.';
ALTER TABLE ONLY ai_provider_keys
ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ALTER TABLE ONLY ai_provider_keys
ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
ALTER TABLE ONLY ai_providers
ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ALTER TABLE ONLY ai_seat_state
ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -6,6 +6,9 @@ type ForeignKeyConstraint string
// ForeignKeyConstraint enums.
const (
ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
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;
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS ai_provider_keys;
DROP TABLE IF EXISTS ai_providers;
DROP TYPE IF EXISTS ai_provider_type;
-- No-op for ALTER TYPE resource_type / api_key_scope ADD VALUE:
-- Postgres does not allow removing enum values safely.
@@ -0,0 +1,67 @@
CREATE TYPE ai_provider_type AS ENUM (
'openai',
'anthropic'
);
CREATE TABLE ai_providers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
type ai_provider_type NOT NULL,
name text NOT NULL
CONSTRAINT ai_providers_name_check
CHECK (name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
display_name text,
enabled boolean NOT NULL DEFAULT TRUE,
deleted boolean NOT NULL DEFAULT FALSE,
base_url text NOT NULL,
settings text,
settings_key_id text REFERENCES dbcrypt_keys(active_key_digest),
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
);
-- Provider names are unique among live rows only. Soft-deleted rows
-- are retained for audit and FK history but do not reserve names.
CREATE UNIQUE INDEX ai_providers_name_unique
ON ai_providers (name)
WHERE deleted = FALSE;
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
CREATE INDEX idx_ai_providers_enabled ON ai_providers (enabled) WHERE deleted = FALSE;
CREATE TABLE ai_provider_keys (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id uuid NOT NULL REFERENCES ai_providers(id) ON DELETE CASCADE,
api_key text NOT NULL,
api_key_key_id text REFERENCES dbcrypt_keys(active_key_digest),
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys (provider_id);
-- Audit support: allow ai_providers and ai_provider_keys to appear in
-- audit_log.resource_type.
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider';
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider_key';
-- API key scopes for ai_provider resources.
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:*';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:create';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:delete';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:read';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:update';
@@ -0,0 +1,56 @@
INSERT INTO ai_providers (
id,
type,
name,
display_name,
enabled,
deleted,
base_url,
settings
) VALUES
(
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
'openai',
'openai',
'OpenAI (Fixture)',
TRUE,
FALSE,
'https://api.openai.com/v1/',
''
),
(
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a02',
'anthropic',
'anthropic-bedrock',
'Anthropic via Bedrock (Fixture)',
TRUE,
FALSE,
'https://bedrock-runtime.us-west-2.amazonaws.com/',
'{"bedrock_region":"us-west-2","bedrock_model":"global.anthropic.claude-sonnet-4-5-20250929-v1:0","bedrock_access_key":"fixture-bedrock-access-key","bedrock_access_key_secret":"fixture-bedrock-access-key-secret"}'
),
(
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a03',
'openai',
'openai-deleted',
'OpenAI (Deleted Fixture)',
FALSE,
TRUE,
'https://api.openai.com/v1/',
''
);
INSERT INTO ai_provider_keys (
id,
provider_id,
api_key
) VALUES
(
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b01',
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
'fixture-openai-key'
),
(
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b02',
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
'fixture-openai-key-failover'
);
+112 -2
View File
@@ -16,6 +16,64 @@ import (
"github.com/sqlc-dev/pqtype"
)
type AIProviderType string
const (
AiProviderTypeOpenai AIProviderType = "openai"
AiProviderTypeAnthropic AIProviderType = "anthropic"
)
func (e *AIProviderType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = AIProviderType(s)
case string:
*e = AIProviderType(s)
default:
return fmt.Errorf("unsupported scan type for AIProviderType: %T", src)
}
return nil
}
type NullAIProviderType struct {
AIProviderType AIProviderType `json:"ai_provider_type"`
Valid bool `json:"valid"` // Valid is true if AIProviderType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullAIProviderType) Scan(value interface{}) error {
if value == nil {
ns.AIProviderType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.AIProviderType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullAIProviderType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.AIProviderType), nil
}
func (e AIProviderType) Valid() bool {
switch e {
case AiProviderTypeOpenai,
AiProviderTypeAnthropic:
return true
}
return false
}
func AllAIProviderTypeValues() []AIProviderType {
return []AIProviderType{
AiProviderTypeOpenai,
AiProviderTypeAnthropic,
}
}
type APIKeyScope string
const (
@@ -230,6 +288,11 @@ const (
ApiKeyScopeAiModelPrice APIKeyScope = "ai_model_price:*"
ApiKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
ApiKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
ApiKeyScopeAiProvider APIKeyScope = "ai_provider:*"
ApiKeyScopeAiProviderCreate APIKeyScope = "ai_provider:create"
ApiKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
)
func (e *APIKeyScope) Scan(src interface{}) error {
@@ -479,7 +542,12 @@ func (e APIKeyScope) Valid() bool {
ApiKeyScopeAiSeatRead,
ApiKeyScopeAiModelPrice,
ApiKeyScopeAiModelPriceRead,
ApiKeyScopeAiModelPriceUpdate:
ApiKeyScopeAiModelPriceUpdate,
ApiKeyScopeAiProvider,
ApiKeyScopeAiProviderCreate,
ApiKeyScopeAiProviderDelete,
ApiKeyScopeAiProviderRead,
ApiKeyScopeAiProviderUpdate:
return true
}
return false
@@ -698,6 +766,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
ApiKeyScopeAiModelPrice,
ApiKeyScopeAiModelPriceRead,
ApiKeyScopeAiModelPriceUpdate,
ApiKeyScopeAiProvider,
ApiKeyScopeAiProviderCreate,
ApiKeyScopeAiProviderDelete,
ApiKeyScopeAiProviderRead,
ApiKeyScopeAiProviderUpdate,
}
}
@@ -3225,6 +3298,8 @@ const (
ResourceTypeAiSeat ResourceType = "ai_seat"
ResourceTypeChat ResourceType = "chat"
ResourceTypeUserSecret ResourceType = "user_secret"
ResourceTypeAiProvider ResourceType = "ai_provider"
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -3292,7 +3367,9 @@ func (e ResourceType) Valid() bool {
ResourceTypeTask,
ResourceTypeAiSeat,
ResourceTypeChat,
ResourceTypeUserSecret:
ResourceTypeUserSecret,
ResourceTypeAiProvider,
ResourceTypeAiProviderKey:
return true
}
return false
@@ -3329,6 +3406,8 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeAiSeat,
ResourceTypeChat,
ResourceTypeUserSecret,
ResourceTypeAiProvider,
ResourceTypeAiProviderKey,
}
}
@@ -4299,6 +4378,37 @@ type AIBridgeUserPrompt struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.
type AIProvider struct {
ID uuid.UUID `db:"id" json:"id"`
Type AIProviderType `db:"type" json:"type"`
Name string `db:"name" json:"name"`
// Optional human-readable label. When NULL, callers should fall back to name.
DisplayName sql.NullString `db:"display_name" json:"display_name"`
Enabled bool `db:"enabled" json:"enabled"`
// Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.
Deleted bool `db:"deleted" json:"deleted"`
BaseUrl string `db:"base_url" json:"base_url"`
// Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.
Settings sql.NullString `db:"settings" json:"settings"`
// The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.
type AIProviderKey struct {
ID uuid.UUID `db:"id" json:"id"`
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
// API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.
APIKey string `db:"api_key" json:"api_key"`
// The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
type APIKey struct {
ID string `db:"id" json:"id"`
// hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.
+29
View File
@@ -99,6 +99,8 @@ type sqlcQuerier interface {
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error)
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error
DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error
@@ -246,6 +248,21 @@ type sqlcQuerier interface {
GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error)
GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error)
GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error)
GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error)
GetAIProviderByName(ctx context.Context, name string) (AIProvider, error)
GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error)
// Returns every AI provider key row, including those belonging to a
// soft-deleted provider, so the dbcrypt key rotation utility can
// re-encrypt their api_key and clear references to retired keys.
GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error)
// Returns all keys for a provider, ordered by created_at ASC so the
// oldest key is returned first. AI Bridge currently uses the oldest
// key per provider; multiple keys are stored to support future
// failover and rotation flows.
GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error)
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
// unless include_deleted or include_disabled is set.
GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error)
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
// there is no unique constraint on empty token names
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
@@ -866,6 +883,8 @@ type sqlcQuerier interface {
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error)
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error)
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error)
InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error)
InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
// We use the organization_id as the id
// for simplicity since all users is
@@ -1097,6 +1116,7 @@ type sqlcQuerier interface {
UnpinChatByID(ctx context.Context, id uuid.UUID) error
UnsetDefaultChatModelConfigs(ctx context.Context) error
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
@@ -1159,6 +1179,15 @@ type sqlcQuerier interface {
UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error)
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
// Updates only the encrypted columns (api_key, api_key_key_id) and
// the updated_at timestamp on a row. Used by the dbcrypt key
// rotation utility to re-encrypt or decrypt rows in place.
UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error)
// Updates only the encrypted columns (settings, settings_key_id) and
// the updated_at timestamp on a row, regardless of its deleted flag.
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
// rows in place.
UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error)
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
// Optimistic lock: only update the row if the refresh token in the database
// still matches the one we read before attempting the refresh. This prevents
+493
View File
@@ -111,6 +111,499 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump
return err
}
const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec
DELETE FROM
ai_provider_keys
WHERE
id = $1::uuid
`
func (q *sqlQuerier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAIProviderKey, id)
return err
}
const getAIProviderKeyByID = `-- name: GetAIProviderKeyByID :one
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, getAIProviderKeyByID, id)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getAIProviderKeys = `-- name: GetAIProviderKeys :many
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
ORDER BY
provider_id ASC,
created_at ASC,
id ASC
`
// Returns every AI provider key row, including those belonging to a
// soft-deleted provider, so the dbcrypt key rotation utility can
// re-encrypt their api_key and clear references to retired keys.
func (q *sqlQuerier) GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProviderKey
for rows.Next() {
var i AIProviderKey
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&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 getAIProviderKeysByProviderID = `-- name: GetAIProviderKeysByProviderID :many
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
WHERE
provider_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
// Returns all keys for a provider, ordered by created_at ASC so the
// oldest key is returned first. AI Bridge currently uses the oldest
// key per provider; multiple keys are stored to support future
// failover and rotation flows.
func (q *sqlQuerier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderID, providerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProviderKey
for rows.Next() {
var i AIProviderKey
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&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 insertAIProviderKey = `-- name: InsertAIProviderKey :one
INSERT INTO ai_provider_keys (
id,
provider_id,
api_key,
api_key_key_id,
created_at,
updated_at
) VALUES (
$1::uuid,
$2::uuid,
$3::text,
$4::text,
$5::timestamptz,
$6::timestamptz
)
RETURNING
id, provider_id, api_key, api_key_key_id, created_at, updated_at
`
type InsertAIProviderKeyParams struct {
ID uuid.UUID `db:"id" json:"id"`
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, insertAIProviderKey,
arg.ID,
arg.ProviderID,
arg.APIKey,
arg.ApiKeyKeyID,
arg.CreatedAt,
arg.UpdatedAt,
)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateEncryptedAIProviderKey = `-- name: UpdateEncryptedAIProviderKey :one
UPDATE
ai_provider_keys
SET
api_key = $1::text,
api_key_key_id = $2::text,
updated_at = NOW()
WHERE
id = $3::uuid
RETURNING
id, provider_id, api_key, api_key_key_id, created_at, updated_at
`
type UpdateEncryptedAIProviderKeyParams struct {
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
// Updates only the encrypted columns (api_key, api_key_key_id) and
// the updated_at timestamp on a row. Used by the dbcrypt key
// rotation utility to re-encrypt or decrypt rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteAIProviderByID = `-- name: DeleteAIProviderByID :exec
UPDATE
ai_providers
SET
deleted = TRUE,
enabled = FALSE,
updated_at = NOW()
WHERE
id = $1::uuid AND deleted = FALSE
`
func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAIProviderByID, id)
return err
}
const getAIProviderByID = `-- name: GetAIProviderByID :one
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
ai_providers
WHERE
id = $1::uuid AND deleted = FALSE
`
func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, getAIProviderByID, id)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getAIProviderByName = `-- name: GetAIProviderByName :one
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
ai_providers
WHERE
name = $1::text AND deleted = FALSE
`
func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, getAIProviderByName, name)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getAIProviders = `-- name: GetAIProviders :many
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
ai_providers
WHERE
($1::boolean OR NOT deleted)
AND ($2::boolean OR enabled)
ORDER BY
name ASC
`
type GetAIProvidersParams struct {
IncludeDeleted bool `db:"include_deleted" json:"include_deleted"`
IncludeDisabled bool `db:"include_disabled" json:"include_disabled"`
}
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
// unless include_deleted or include_disabled is set.
func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error) {
rows, err := q.db.QueryContext(ctx, getAIProviders, arg.IncludeDeleted, arg.IncludeDisabled)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProvider
for rows.Next() {
var i AIProvider
if err := rows.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&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 insertAIProvider = `-- name: InsertAIProvider :one
INSERT INTO ai_providers (
id,
type,
name,
display_name,
enabled,
base_url,
settings,
settings_key_id
) VALUES (
$1::uuid,
$2::ai_provider_type,
$3::text,
$4::text,
$5::boolean,
$6::text,
$7::text,
$8::text
)
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`
type InsertAIProviderParams struct {
ID uuid.UUID `db:"id" json:"id"`
Type AIProviderType `db:"type" json:"type"`
Name string `db:"name" json:"name"`
DisplayName sql.NullString `db:"display_name" json:"display_name"`
Enabled bool `db:"enabled" json:"enabled"`
BaseUrl string `db:"base_url" json:"base_url"`
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
}
func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, insertAIProvider,
arg.ID,
arg.Type,
arg.Name,
arg.DisplayName,
arg.Enabled,
arg.BaseUrl,
arg.Settings,
arg.SettingsKeyID,
)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateAIProvider = `-- name: UpdateAIProvider :one
UPDATE
ai_providers
SET
display_name = $1::text,
enabled = $2::boolean,
base_url = $3::text,
settings = $4::text,
settings_key_id = $5::text,
updated_at = NOW()
WHERE
id = $6::uuid AND deleted = FALSE
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`
type UpdateAIProviderParams struct {
DisplayName sql.NullString `db:"display_name" json:"display_name"`
Enabled bool `db:"enabled" json:"enabled"`
BaseUrl string `db:"base_url" json:"base_url"`
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, updateAIProvider,
arg.DisplayName,
arg.Enabled,
arg.BaseUrl,
arg.Settings,
arg.SettingsKeyID,
arg.ID,
)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateEncryptedAIProviderSettings = `-- name: UpdateEncryptedAIProviderSettings :one
UPDATE
ai_providers
SET
settings = $1::text,
settings_key_id = $2::text,
updated_at = NOW()
WHERE
id = $3::uuid
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`
type UpdateEncryptedAIProviderSettingsParams struct {
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
// Updates only the encrypted columns (settings, settings_key_id) and
// the updated_at timestamp on a row, regardless of its deleted flag.
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
// rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderSettings, arg.Settings, arg.SettingsKeyID, arg.ID)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const calculateAIBridgeInterceptionsTelemetrySummary = `-- name: CalculateAIBridgeInterceptionsTelemetrySummary :one
WITH interceptions_in_range AS (
-- Get all matching interceptions in the given timeframe.
@@ -0,0 +1,75 @@
-- name: GetAIProviderKeyByID :one
SELECT
*
FROM
ai_provider_keys
WHERE
id = @id::uuid;
-- name: GetAIProviderKeysByProviderID :many
-- Returns all keys for a provider, ordered by created_at ASC so the
-- oldest key is returned first. AI Bridge currently uses the oldest
-- key per provider; multiple keys are stored to support future
-- failover and rotation flows.
SELECT
*
FROM
ai_provider_keys
WHERE
provider_id = @provider_id::uuid
ORDER BY
created_at ASC,
id ASC;
-- name: GetAIProviderKeys :many
-- Returns every AI provider key row, including those belonging to a
-- soft-deleted provider, so the dbcrypt key rotation utility can
-- re-encrypt their api_key and clear references to retired keys.
SELECT
*
FROM
ai_provider_keys
ORDER BY
provider_id ASC,
created_at ASC,
id ASC;
-- name: InsertAIProviderKey :one
INSERT INTO ai_provider_keys (
id,
provider_id,
api_key,
api_key_key_id,
created_at,
updated_at
) VALUES (
@id::uuid,
@provider_id::uuid,
@api_key::text,
sqlc.narg('api_key_key_id')::text,
@created_at::timestamptz,
@updated_at::timestamptz
)
RETURNING
*;
-- name: DeleteAIProviderKey :exec
DELETE FROM
ai_provider_keys
WHERE
id = @id::uuid;
-- name: UpdateEncryptedAIProviderKey :one
-- Updates only the encrypted columns (api_key, api_key_key_id) and
-- the updated_at timestamp on a row. Used by the dbcrypt key
-- rotation utility to re-encrypt or decrypt rows in place.
UPDATE
ai_provider_keys
SET
api_key = @api_key::text,
api_key_key_id = sqlc.narg('api_key_key_id')::text,
updated_at = NOW()
WHERE
id = @id::uuid
RETURNING
*;
+92
View File
@@ -0,0 +1,92 @@
-- name: GetAIProviderByID :one
SELECT
*
FROM
ai_providers
WHERE
id = @id::uuid AND deleted = FALSE;
-- name: GetAIProviderByName :one
SELECT
*
FROM
ai_providers
WHERE
name = @name::text AND deleted = FALSE;
-- name: GetAIProviders :many
-- Returns AI provider rows. Soft-deleted and disabled rows are excluded
-- unless include_deleted or include_disabled is set.
SELECT
*
FROM
ai_providers
WHERE
(@include_deleted::boolean OR NOT deleted)
AND (@include_disabled::boolean OR enabled)
ORDER BY
name ASC;
-- name: InsertAIProvider :one
INSERT INTO ai_providers (
id,
type,
name,
display_name,
enabled,
base_url,
settings,
settings_key_id
) VALUES (
@id::uuid,
@type::ai_provider_type,
@name::text,
sqlc.narg('display_name')::text,
@enabled::boolean,
@base_url::text,
sqlc.narg('settings')::text,
sqlc.narg('settings_key_id')::text
)
RETURNING
*;
-- name: UpdateAIProvider :one
UPDATE
ai_providers
SET
display_name = sqlc.narg('display_name')::text,
enabled = @enabled::boolean,
base_url = @base_url::text,
settings = sqlc.narg('settings')::text,
settings_key_id = sqlc.narg('settings_key_id')::text,
updated_at = NOW()
WHERE
id = @id::uuid AND deleted = FALSE
RETURNING
*;
-- name: DeleteAIProviderByID :exec
UPDATE
ai_providers
SET
deleted = TRUE,
enabled = FALSE,
updated_at = NOW()
WHERE
id = @id::uuid AND deleted = FALSE;
-- name: UpdateEncryptedAIProviderSettings :one
-- Updates only the encrypted columns (settings, settings_key_id) and
-- the updated_at timestamp on a row, regardless of its deleted flag.
-- Used by the dbcrypt key rotation utility to re-encrypt or decrypt
-- rows in place.
UPDATE
ai_providers
SET
settings = sqlc.narg('settings')::text,
settings_key_id = sqlc.narg('settings_key_id')::text,
updated_at = NOW()
WHERE
id = @id::uuid
RETURNING
*;
+3
View File
@@ -240,6 +240,9 @@ sql:
aibridge_token_usage: AIBridgeTokenUsage
aibridge_user_prompt: AIBridgeUserPrompt
aibridge_model_thought: AIBridgeModelThought
ai_provider: AIProvider
ai_provider_key: AIProviderKey
ai_provider_type: AIProviderType
mcp_server_config: MCPServerConfig
mcp_server_configs: MCPServerConfigs
mcp_server_user_token: MCPServerUserToken
+3
View File
@@ -8,6 +8,8 @@ type UniqueConstraint string
const (
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
UniqueAiModelPricesPkey UniqueConstraint = "ai_model_prices_pkey" // ALTER TABLE ONLY ai_model_prices ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
UniqueAiProviderKeysPkey UniqueConstraint = "ai_provider_keys_pkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
UniqueAiProvidersPkey UniqueConstraint = "ai_providers_pkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
UniqueAiSeatStatePkey UniqueConstraint = "ai_seat_state_pkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
UniqueAibridgeInterceptionsPkey UniqueConstraint = "aibridge_interceptions_pkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
@@ -130,6 +132,7 @@ const (
UniqueWorkspaceResourceMetadataPkey UniqueConstraint = "workspace_resource_metadata_pkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_pkey PRIMARY KEY (id);
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);
UniqueAiProvidersNameUnique UniqueConstraint = "ai_providers_name_unique" // CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
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);
UniqueIndexChatDebugRunsIDChat UniqueConstraint = "idx_chat_debug_runs_id_chat" // CREATE UNIQUE INDEX idx_chat_debug_runs_id_chat ON chat_debug_runs USING btree (id, chat_id);
UniqueIndexChatDebugStepsRunStep UniqueConstraint = "idx_chat_debug_steps_run_step" // CREATE UNIQUE INDEX idx_chat_debug_steps_run_step ON chat_debug_steps USING btree (run_id, step_number);
+11
View File
@@ -23,6 +23,16 @@ var (
Type: "ai_model_price",
}
// ResourceAIProvider
// Valid Actions
// - "ActionCreate" :: create an AI provider
// - "ActionDelete" :: delete an AI provider
// - "ActionRead" :: read AI provider configuration
// - "ActionUpdate" :: update an AI provider
ResourceAIProvider = Object{
Type: "ai_provider",
}
// ResourceAiSeat
// Valid Actions
// - "ActionCreate" :: record AI seat usage
@@ -450,6 +460,7 @@ func AllResources() []Objecter {
return []Objecter{
ResourceWildcard,
ResourceAiModelPrice,
ResourceAIProvider,
ResourceAiSeat,
ResourceAibridgeInterception,
ResourceApiKey,
+9
View File
@@ -398,6 +398,15 @@ var RBACPermissions = map[string]PermissionDefinition{
ActionUpdate: "update AI model prices",
},
},
"ai_provider": {
Name: "AIProvider",
Actions: map[Action]ActionDefinition{
ActionRead: "read AI provider configuration",
ActionCreate: "create an AI provider",
ActionUpdate: "update an AI provider",
ActionDelete: "delete an AI provider",
},
},
"ai_seat": {
Actions: map[Action]ActionDefinition{
ActionCreate: "record AI seat usage",
+19
View File
@@ -1105,6 +1105,25 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
// Only owners can manage AI providers. Provider
// configuration is deployment-wide and includes secret
// material (api_key, settings) so it is not exposed to
// org admins or auditors.
Name: "AIProviders",
Actions: crud,
Resource: rbac.ResourceAIProvider,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
memberMe, agentsAccessUser,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
Name: "BoundaryUsage",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
+12
View File
@@ -9,6 +9,10 @@ package rbac
const (
ScopeAiModelPriceRead ScopeName = "ai_model_price:read"
ScopeAiModelPriceUpdate ScopeName = "ai_model_price:update"
ScopeAiProviderCreate ScopeName = "ai_provider:create"
ScopeAiProviderDelete ScopeName = "ai_provider:delete"
ScopeAiProviderRead ScopeName = "ai_provider:read"
ScopeAiProviderUpdate ScopeName = "ai_provider:update"
ScopeAiSeatCreate ScopeName = "ai_seat:create"
ScopeAiSeatRead ScopeName = "ai_seat:read"
ScopeAibridgeInterceptionCreate ScopeName = "aibridge_interception:create"
@@ -177,6 +181,10 @@ func (e ScopeName) Valid() bool {
ScopeName("no_user_data"),
ScopeAiModelPriceRead,
ScopeAiModelPriceUpdate,
ScopeAiProviderCreate,
ScopeAiProviderDelete,
ScopeAiProviderRead,
ScopeAiProviderUpdate,
ScopeAiSeatCreate,
ScopeAiSeatRead,
ScopeAibridgeInterceptionCreate,
@@ -346,6 +354,10 @@ func AllScopeNameValues() []ScopeName {
ScopeName("no_user_data"),
ScopeAiModelPriceRead,
ScopeAiModelPriceUpdate,
ScopeAiProviderCreate,
ScopeAiProviderDelete,
ScopeAiProviderRead,
ScopeAiProviderUpdate,
ScopeAiSeatCreate,
ScopeAiSeatRead,
ScopeAibridgeInterceptionCreate,
+5
View File
@@ -9,6 +9,11 @@ const (
APIKeyScopeAiModelPriceAll APIKeyScope = "ai_model_price:*"
APIKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
APIKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
APIKeyScopeAiProviderAll APIKeyScope = "ai_provider:*"
APIKeyScopeAiProviderCreate APIKeyScope = "ai_provider:create"
APIKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
APIKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
APIKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
APIKeyScopeAiSeatAll APIKeyScope = "ai_seat:*"
APIKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create"
APIKeyScopeAiSeatRead APIKeyScope = "ai_seat:read"
+11 -5
View File
@@ -43,11 +43,13 @@ const (
ResourceTypeWorkspaceAgent ResourceType = "workspace_agent"
// Deprecated: Workspace App connections are now included in the
// connection log.
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
ResourceTypeTask ResourceType = "task"
ResourceTypeAISeat ResourceType = "ai_seat"
ResourceTypeChat ResourceType = "chat"
ResourceTypeUserSecret ResourceType = "user_secret"
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
ResourceTypeTask ResourceType = "task"
ResourceTypeAISeat ResourceType = "ai_seat"
ResourceTypeAIProvider ResourceType = "ai_provider"
ResourceTypeAIProviderKey ResourceType = "ai_provider_key"
ResourceTypeChat ResourceType = "chat"
ResourceTypeUserSecret ResourceType = "user_secret"
)
func (r ResourceType) FriendlyString() string {
@@ -108,6 +110,10 @@ func (r ResourceType) FriendlyString() string {
return "task"
case ResourceTypeAISeat:
return "ai seat"
case ResourceTypeAIProvider:
return "ai provider"
case ResourceTypeAIProviderKey:
return "ai provider key"
case ResourceTypeChat:
return "chat"
case ResourceTypeUserSecret:
+4 -4
View File
@@ -4005,7 +4005,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Proxy Domain Allowlist",
Description: "Deprecated: This value is now derived automatically from the configured AI Bridge providers' base URLs. Setting this value has no effect. This option will be removed in a future release.",
Description: "Deprecated: This value is now derived automatically from the configured AI providers' base URLs. Setting this value has no effect. This option will be removed in a future release.",
Flag: "aibridge-proxy-domain-allowlist",
Env: "CODER_AIBRIDGE_PROXY_DOMAIN_ALLOWLIST",
Value: &c.AI.BridgeProxyConfig.DomainAllowlist,
@@ -4147,7 +4147,7 @@ type AIBridgeConfig struct {
LegacyBedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
// Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
// env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.
Providers []AIBridgeProviderConfig `json:"providers,omitempty"`
Providers []AIProviderConfig `json:"providers,omitempty"`
// Deprecated: Injected MCP in AI Bridge is deprecated and will be removed in a future release.
InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"`
Retention serpent.Duration `json:"retention" typescript:",notnull"`
@@ -4187,10 +4187,10 @@ type AIBridgeBedrockConfig struct {
SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"`
}
// AIBridgeProviderConfig represents a single AI Bridge provider instance,
// AIProviderConfig represents a single AI provider instance,
// parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
// This follows the same indexed pattern as ExternalAuthConfig.
type AIBridgeProviderConfig struct {
type AIProviderConfig struct {
// Type is the provider type: "openai", "anthropic", or "copilot".
Type string `json:"type"`
// Name is the unique instance identifier used for routing.
+2
View File
@@ -6,6 +6,7 @@ type RBACResource string
const (
ResourceWildcard RBACResource = "*"
ResourceAiModelPrice RBACResource = "ai_model_price"
ResourceAIProvider RBACResource = "ai_provider"
ResourceAiSeat RBACResource = "ai_seat"
ResourceAibridgeInterception RBACResource = "aibridge_interception"
ResourceApiKey RBACResource = "api_key"
@@ -80,6 +81,7 @@ const (
var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceWildcard: {},
ResourceAiModelPrice: {ActionRead, ActionUpdate},
ResourceAIProvider: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceAiSeat: {ActionCreate, ActionRead},
ResourceAibridgeInterception: {ActionCreate, ActionRead, ActionUpdate},
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
+2
View File
@@ -15,6 +15,8 @@ We track the following resources:
| <b>Resource<b> | | |
|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AIProvider<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>base_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>settings</td><td>true</td></tr><tr><td>settings_key_id</td><td>false</td></tr><tr><td>type</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| AIProviderKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_key</td><td>true</td></tr><tr><td>api_key_key_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>provider_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| APIKey<br><i>login, logout, register, create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AiSeatState<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>first_used_at</td><td>true</td></tr><tr><td>last_event_description</td><td>true</td></tr><tr><td>last_event_type</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
+20 -20
View File
@@ -193,10 +193,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -326,10 +326,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -459,10 +459,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -554,10 +554,10 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -960,9 +960,9 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+56 -56
View File
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -865,11 +865,11 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | `*`, `ai_model_price`, `ai_seat`, `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` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `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` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+23
View File
@@ -29,6 +29,8 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
"Task": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"AiSeatState": {codersdk.AuditActionCreate},
"AIProvider": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
"AIProviderKey": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
}
@@ -367,6 +369,27 @@ var auditableResourcesTypes = map[any]map[string]Action{
"last_used_at": ActionIgnore,
"updated_at": ActionIgnore,
},
&database.AIProvider{}: {
"id": ActionTrack,
"type": ActionTrack,
"name": ActionTrack,
"display_name": ActionTrack,
"enabled": ActionTrack,
"deleted": ActionTrack,
"base_url": ActionTrack,
"settings": ActionSecret, // Encrypted JSON blob may contain provider secrets (e.g. Bedrock access key + secret).
"settings_key_id": ActionIgnore, // dbcrypt key reference, derivable.
"created_at": ActionIgnore, // Implicit; not useful in a diff.
"updated_at": ActionIgnore, // Changes; not useful in a diff.
},
&database.AIProviderKey{}: {
"id": ActionTrack,
"provider_id": ActionTrack,
"api_key": ActionSecret, // Provider API key, never expose in audit diffs.
"api_key_key_id": ActionIgnore, // dbcrypt key reference, derivable.
"created_at": ActionIgnore, // Implicit; not useful in a diff.
"updated_at": ActionIgnore, // Changes; not useful in a diff.
},
&database.TaskTable{}: {
"id": ActionTrack,
"organization_id": ActionIgnore, // Never changes.
+3 -3
View File
@@ -44,7 +44,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*ai
return srv, nil
}
// buildProviders constructs the list of aibridge providers from config.
// buildProviders constructs the list of AI providers from config.
// It merges legacy single-provider env vars and indexed provider configs:
// 1. Legacy providers (from CODER_AIBRIDGE_OPENAI_KEY, etc.) are added first.
// If a legacy name conflicts with an indexed provider, startup fails with
@@ -171,9 +171,9 @@ func buildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
}
// bedrockConfigFromProvider converts Bedrock fields from an indexed
// AIBridgeProviderConfig into an aibridge AWSBedrockConfig.
// AIProviderConfig into an aibridge AWSBedrockConfig.
// Returns nil if no Bedrock fields are set.
func bedrockConfigFromProvider(p codersdk.AIBridgeProviderConfig) *aibridge.AWSBedrockConfig {
func bedrockConfigFromProvider(p codersdk.AIProviderConfig) *aibridge.AWSBedrockConfig {
if p.BedrockRegion == "" && p.BedrockBaseURL == "" && len(p.BedrockAccessKeys) == 0 && len(p.BedrockAccessKeySecrets) == 0 {
return nil
}
+10 -10
View File
@@ -42,7 +42,7 @@ func TestBuildProviders(t *testing.T) {
t.Run("IndexedOnly", func(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{
Type: aibridge.ProviderAnthropic,
Name: "anthropic-zdr",
@@ -71,7 +71,7 @@ func TestBuildProviders(t *testing.T) {
t.Run("LegacyOpenAIConflictsWithIndexed", func(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-indexed"}},
},
}
@@ -85,7 +85,7 @@ func TestBuildProviders(t *testing.T) {
t.Run("LegacyAnthropicConflictsWithIndexed", func(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: aibridge.ProviderAnthropic, Keys: []string{"sk-indexed"}},
},
}
@@ -99,7 +99,7 @@ func TestBuildProviders(t *testing.T) {
t.Run("MixedLegacyAndIndexed", func(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderAnthropic, Name: "anthropic-zdr", Keys: []string{"sk-zdr"}},
},
}
@@ -151,7 +151,7 @@ func TestBuildProviders(t *testing.T) {
t.Run("UnknownType", func(t *testing.T) {
t.Parallel()
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: "gemini", Name: "gemini-pro"},
},
}
@@ -166,7 +166,7 @@ func TestBuildProviders(t *testing.T) {
// Copilot providers can target any of the three GitHub
// Copilot API hosts via an explicit BASE_URL.
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderCopilot, Name: aibridge.ProviderCopilot, DumpDir: "/tmp/copilot-dump"},
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotBusiness, BaseURL: "https://" + agplaibridge.HostCopilotBusiness},
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotEnterprise, BaseURL: "https://" + agplaibridge.HostCopilotEnterprise},
@@ -190,7 +190,7 @@ func TestBuildProviders(t *testing.T) {
// ChatGPT is an OpenAI-compatible provider with a custom
// base URL. Admins configure it as an indexed openai provider.
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: agplaibridge.ProviderChatGPT, BaseURL: agplaibridge.BaseURLChatGPT},
},
}
@@ -219,7 +219,7 @@ func TestDomainsFromProviders(t *testing.T) {
t.Parallel()
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "openai", Keys: []string{"k"}},
{Type: aibridge.ProviderAnthropic, Name: "anthropic", Keys: []string{"k"}},
{Type: aibridge.ProviderOpenAI, Name: "custom", Keys: []string{"k"}, BaseURL: "https://custom-llm.example.com:8443/api"},
@@ -243,7 +243,7 @@ func TestDomainsFromProviders(t *testing.T) {
t.Parallel()
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "first", Keys: []string{"k"}, BaseURL: "https://api.example.com/v1"},
{Type: aibridge.ProviderOpenAI, Name: "second", Keys: []string{"k"}, BaseURL: "https://api.example.com/v2"},
},
@@ -268,7 +268,7 @@ func TestDomainsFromProviders(t *testing.T) {
t.Parallel()
providers, err := buildProviders(codersdk.AIBridgeConfig{
Providers: []codersdk.AIBridgeProviderConfig{
Providers: []codersdk.AIProviderConfig{
{Type: aibridge.ProviderOpenAI, Name: "provider", Keys: []string{"k"}, BaseURL: "https://API.Example.COM/v1"},
},
})
+1 -1
View File
@@ -168,7 +168,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
if bridgeEnabled || proxyEnabled {
providers, err := buildProviders(options.DeploymentValues.AI.BridgeConfig)
if err != nil {
return nil, nil, xerrors.Errorf("build aibridge providers: %w", err)
return nil, nil, xerrors.Errorf("build AI providers: %w", err)
}
// In-memory aibridge daemon.
+6
View File
@@ -12,6 +12,12 @@ export const RBACResourceActions: Partial<
read: "read AI model prices",
update: "update AI model prices",
},
ai_provider: {
create: "create an AI provider",
delete: "delete an AI provider",
read: "read AI provider configuration",
update: "update an AI provider",
},
ai_seat: {
create: "record AI seat usage",
read: "read AI seat state",
+46 -30
View File
@@ -57,7 +57,7 @@ export interface AIBridgeConfig {
* Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
* env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.
*/
readonly providers?: readonly AIBridgeProviderConfig[];
readonly providers?: readonly AIProviderConfig[];
/**
* @deprecated Injected MCP in AI Bridge is deprecated and will be removed in a future release.
*/
@@ -129,35 +129,6 @@ export interface AIBridgeOpenAIConfig {
readonly key: string;
}
// From codersdk/deployment.go
/**
* AIBridgeProviderConfig represents a single AI Bridge provider instance,
* parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
* This follows the same indexed pattern as ExternalAuthConfig.
*/
export interface AIBridgeProviderConfig {
/**
* Type is the provider type: "openai", "anthropic", or "copilot".
*/
readonly type: string;
/**
* Name is the unique instance identifier used for routing.
* Defaults to Type if not provided.
*/
readonly name: string;
/**
* BaseURL is the base URL of the upstream provider API.
*/
readonly base_url: string;
/**
* DumpDir is the directory path for dumping API requests and responses.
*/
readonly dump_dir?: string;
readonly bedrock_region?: string;
readonly bedrock_model?: string;
readonly bedrock_small_fast_model?: string;
}
// From codersdk/deployment.go
export interface AIBridgeProxyConfig {
readonly enabled: boolean;
@@ -327,6 +298,35 @@ export interface AIConfig {
readonly chat?: ChatConfig;
}
// From codersdk/deployment.go
/**
* AIProviderConfig represents a single AI provider instance,
* parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
* This follows the same indexed pattern as ExternalAuthConfig.
*/
export interface AIProviderConfig {
/**
* Type is the provider type: "openai", "anthropic", or "copilot".
*/
readonly type: string;
/**
* Name is the unique instance identifier used for routing.
* Defaults to Type if not provided.
*/
readonly name: string;
/**
* BaseURL is the base URL of the upstream provider API.
*/
readonly base_url: string;
/**
* DumpDir is the directory path for dumping API requests and responses.
*/
readonly dump_dir?: string;
readonly bedrock_region?: string;
readonly bedrock_model?: string;
readonly bedrock_small_fast_model?: string;
}
// From codersdk/allowlist.go
/**
* APIAllowListTarget represents a single allow-list entry using the canonical
@@ -362,6 +362,11 @@ export type APIKeyScope =
| "ai_model_price:*"
| "ai_model_price:read"
| "ai_model_price:update"
| "ai_provider:*"
| "ai_provider:create"
| "ai_provider:delete"
| "ai_provider:read"
| "ai_provider:update"
| "ai_seat:*"
| "ai_seat:create"
| "ai_seat:read"
@@ -577,6 +582,11 @@ export const APIKeyScopes: APIKeyScope[] = [
"ai_model_price:*",
"ai_model_price:read",
"ai_model_price:update",
"ai_provider:*",
"ai_provider:create",
"ai_provider:delete",
"ai_provider:read",
"ai_provider:update",
"ai_seat:*",
"ai_seat:create",
"ai_seat:read",
@@ -6432,6 +6442,7 @@ export const RBACActions: RBACAction[] = [
// From codersdk/rbacresources_gen.go
export type RBACResource =
| "ai_provider"
| "ai_model_price"
| "ai_seat"
| "aibridge_interception"
@@ -6480,6 +6491,7 @@ export type RBACResource =
| "workspace_proxy";
export const RBACResources: RBACResource[] = [
"ai_provider",
"ai_model_price",
"ai_seat",
"aibridge_interception",
@@ -6639,6 +6651,8 @@ export interface ResolveAutostartResponse {
// From codersdk/audit.go
export type ResourceType =
| "ai_provider"
| "ai_provider_key"
| "ai_seat"
| "api_key"
| "chat"
@@ -6670,6 +6684,8 @@ export type ResourceType =
| "workspace_proxy";
export const ResourceTypes: ResourceType[] = [
"ai_provider",
"ai_provider_key",
"ai_seat",
"api_key",
"chat",