diff --git a/cli/aibridged_internal_test.go b/cli/aibridged_internal_test.go index 6b3e1eb7ac..4f1c969027 100644 --- a/cli/aibridged_internal_test.go +++ b/cli/aibridged_internal_test.go @@ -33,7 +33,7 @@ func buildFromEnv(t *testing.T, cfg codersdk.AIBridgeConfig) ([]aibridge.Provide db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitShort) logger := slogtest.Make(t, nil) - if err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, logger); err != nil { + if _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, logger); err != nil { return nil, err } providers, _, err := BuildProviders(ctx, db, cfg, logger) diff --git a/cli/server.go b/cli/server.go index b2fa89fd3b..9d9441a947 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1024,14 +1024,19 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. //nolint:gocritic // Production timeout, not a test wait. aibridgeInitCtx, aibridgeInitCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) defer aibridgeInitCancel() - if err := coderd.SeedAIProvidersFromEnv( + ineffective, err := coderd.SeedAIProvidersFromEnv( aibridgeInitCtx, options.Database, vals.AI.BridgeConfig, logger.Named("aibridge.envseed"), - ); err != nil { + ) + if err != nil { return xerrors.Errorf("seed ai providers from env: %w", err) } + // This runs unconditionally for both editions (enterprise + // reuses this AGPL server runner), so it is the authoritative + // setter for the deprecated-env-drift warning banner. + coderAPI.AIProvidersEnvDrift.Store(ineffective) // In-memory aibridge daemon. Registered on coderd so chatd can // dispatch LLM requests via the in-process transport without diff --git a/coderd/ai_providers_migrate.go b/coderd/ai_providers_migrate.go index 055877ecce..3cf6aad430 100644 --- a/coderd/ai_providers_migrate.go +++ b/coderd/ai_providers_migrate.go @@ -26,9 +26,16 @@ import ( // derived AI provider configuration with rows in the ai_providers // table at server startup. Concurrent server starts are serialized via a // Postgres advisory lock; rows that already exist with a matching -// canonical hash are left alone, missing rows are inserted, and rows -// whose hash differs from the env-derived value cause startup to fail -// with a descriptive error. +// canonical hash are left alone and missing rows are inserted. +// +// AI-provider configuration via CODER_AIBRIDGE_* env vars is deprecated +// in favor of managing providers through the API/UI, where the database +// is the source of truth. When an env-derived provider differs from the +// row already stored in the database ("drift"), startup does NOT fail: +// the env change is ineffective, so the existing row is left untouched, a +// warning is logged, and the returned ineffective flag is set to true so +// callers can surface a banner to admins. Other missing providers in the +// same config are still inserted. // // API keys derived from env vars are inserted into ai_provider_keys at // the time the provider row is first created. We do NOT add env-sourced @@ -45,13 +52,13 @@ func SeedAIProvidersFromEnv( db database.Store, cfg codersdk.AIBridgeConfig, logger slog.Logger, -) error { +) (ineffective bool, err error) { desired, err := providersFromEnv(ctx, cfg, logger) if err != nil { - return xerrors.Errorf("compute providers from env: %w", err) + return false, xerrors.Errorf("compute providers from env: %w", err) } if len(desired) == 0 { - return nil + return false, nil } // Audit entries are attributed to the deployment rather than a user. @@ -65,11 +72,19 @@ func SeedAIProvidersFromEnv( var ( insertedProviders []database.AIProvider insertedKeys []database.AIProviderKey + // drift records whether any env-derived provider differs from a + // row already in the database. driftedNames collects the affected + // provider names for the warning log. Both are reset at the top of + // the InTx closure so a transaction retry recomputes them cleanly. + drift bool + driftedNames []string ) err = db.InTx(func(tx database.Store) error { insertedProviders = insertedProviders[:0] insertedKeys = insertedKeys[:0] + drift = false + driftedNames = driftedNames[:0] // Acquire the advisory lock. The lock is released when the // transaction ends. @@ -136,7 +151,16 @@ func SeedAIProvidersFromEnv( if existingHash == dp.Hash { continue } - return xerrors.Errorf("AI provider %q already exists in the database and differs from the current environment configuration; update the provider through the API or remove the CODER_AIBRIDGE_* env vars to stop seeding it", dp.Name) + // The env-derived config drifted from the stored row. + // CODER_AIBRIDGE_* env config is deprecated, so this is + // not fatal: the database is the source of truth, the + // existing row is left untouched, and the env change is + // ineffective. Flag it (the warning is emitted after the + // transaction commits, like the insert logs below) and keep + // reconciling the remaining providers. + drift = true + driftedNames = append(driftedNames, dp.Name) + continue } row, err := tx.InsertAIProvider(sysCtx, database.InsertAIProviderParams{ @@ -183,7 +207,7 @@ func SeedAIProvidersFromEnv( return nil }, nil) if err != nil { - return err + return false, err } for _, row := range insertedProviders { @@ -201,7 +225,12 @@ func SeedAIProvidersFromEnv( slog.F("api_key", aibridgeutils.MaskSecret(keyRow.APIKey)), ) } - return nil + if drift { + logger.Warn(sysCtx, "deprecated AI provider env configuration is ineffective; the database is the source of truth, so update these providers through the API or remove their CODER_AIBRIDGE_* env vars", + slog.F("providers", driftedNames), + ) + } + return drift, nil } // canonicalAIProvider is the shape we hash to detect drift between the diff --git a/coderd/ai_providers_migrate_test.go b/coderd/ai_providers_migrate_test.go index 89165002b0..6242ca8cb6 100644 --- a/coderd/ai_providers_migrate_test.go +++ b/coderd/ai_providers_migrate_test.go @@ -24,8 +24,9 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitShort) - err := coderd.SeedAIProvidersFromEnv(ctx, db, codersdk.AIBridgeConfig{}, testLogger(t)) + ineffective, err := coderd.SeedAIProvidersFromEnv(ctx, db, codersdk.AIBridgeConfig{}, testLogger(t)) require.NoError(t, err) + require.False(t, ineffective) }) t.Run("LegacyOpenAI", func(t *testing.T) { @@ -40,7 +41,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, } var firstSeedLogs bytes.Buffer - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, capturedLogger(&firstSeedLogs)) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, capturedLogger(&firstSeedLogs)) require.NoError(t, err) // One row exists for "openai". @@ -65,7 +66,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { // Re-running with the same config is a no-op and emits no new // env-seed log lines. var rerunLogs bytes.Buffer - err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, capturedLogger(&rerunLogs)) + _, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, capturedLogger(&rerunLogs)) require.NoError(t, err) require.NotContains(t, rerunLogs.String(), "env-seeded ai provider") @@ -78,7 +79,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { require.Len(t, keys, 1) }) - t.Run("DriftFailsStartup", func(t *testing.T) { + t.Run("DriftIsNonFatalAndFlagged", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitShort) @@ -89,22 +90,38 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Key: serpent.String("sk-original"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + ineffective, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.False(t, ineffective) - // Changing the API key counts as drift: keys are included - // in the canonical hash so operators notice when env-var - // credential changes are ignored by an existing provider. + // Changing the API key counts as drift: keys are included in the + // canonical hash. Because CODER_AIBRIDGE_* config is deprecated, + // drift no longer fails startup; the env change is ineffective, + // the stored row is left untouched, and the call reports it. cfg.LegacyOpenAI.Key = serpent.String("sk-rotated") - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") + ineffective, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) - // Changing the base URL is also real drift. + // The database still holds the original key. + row, err := db.GetAIProviderByName(ctx, "openai") + require.NoError(t, err) + keys, err := db.GetAIProviderKeysByProviderID(ctx, row.ID) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Equal(t, "sk-original", keys[0].APIKey) + + // Changing the base URL is also drift, and also non-fatal. cfg.LegacyOpenAI.Key = serpent.String("sk-original") cfg.LegacyOpenAI.BaseURL = serpent.String("https://api.openai.com/v2") - err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") + ineffective, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) + + // The stored base URL is unchanged. + row, err = db.GetAIProviderByName(ctx, "openai") + require.NoError(t, err) + require.Equal(t, "https://api.openai.com/v1", row.BaseUrl) }) t.Run("BedrockCredentialChangeIsDrift", func(t *testing.T) { @@ -120,24 +137,35 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Model: serpent.String("anthropic.claude-3-5-sonnet"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) - // Rotating the Bedrock access key in env trips the drift - // check so operators know the change did not take effect. + // Rotating the Bedrock access key in env is drift. It is + // non-fatal for the deprecated env path: the call reports the + // change is ineffective and the stored credentials are unchanged. cfg.LegacyBedrock.AccessKey = serpent.String("AKIA-rotated") cfg.LegacyBedrock.AccessKeySecret = serpent.String("secret-rotated") - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") + ineffective, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) - // Changing the Bedrock region (a non-credential field) is - // also real drift. + row, err := db.GetAIProviderByName(ctx, "anthropic") + require.NoError(t, err) + require.Contains(t, row.Settings.String, "AKIA-original") + require.Contains(t, row.Settings.String, "secret-original") + + // Changing the Bedrock region (a non-credential field) is also + // drift, and also non-fatal. cfg.LegacyBedrock.AccessKey = serpent.String("AKIA-original") cfg.LegacyBedrock.AccessKeySecret = serpent.String("secret-original") cfg.LegacyBedrock.Region = serpent.String("us-west-2") - err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") + ineffective, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) + + row, err = db.GetAIProviderByName(ctx, "anthropic") + require.NoError(t, err) + require.Contains(t, row.Settings.String, "us-east-1") }) t.Run("LegacyBedrockOnlyKeepsBedrockSettings", func(t *testing.T) { @@ -156,7 +184,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { SmallFastModel: serpent.String("anthropic.claude-3-5-haiku"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "anthropic") require.NoError(t, err) @@ -191,7 +220,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { cfg := dv.AI.BridgeConfig cfg.LegacyAnthropic.Key = serpent.String("sk-ant-only") - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "anthropic") require.NoError(t, err) @@ -216,7 +246,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Model: serpent.String("anthropic.claude-3-5-sonnet"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "anthropic") require.NoError(t, err) @@ -241,7 +272,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Model: serpent.String("anthropic.claude-3-5-sonnet"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "anthropic") require.NoError(t, err) require.Contains(t, row.Settings.String, "us-east-1") @@ -275,7 +307,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) oa, err := db.GetAIProviderByName(ctx, "primary-openai") require.NoError(t, err) @@ -319,34 +352,55 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) // Reordering keys must not count as drift. The canonical hash // sorts keys before hashing, so equivalent key sets remain // stable across restarts. cfg.Providers[0].Keys = []string{"sk-openai-2", "sk-openai-1"} cfg.Providers[1].Keys = []string{"sk-ant-2", "sk-ant-1"} - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + ineffective, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.False(t, ineffective) - // Changing one key on one provider must block startup even - // when multiple providers are configured. + // Changing one key on one provider is drift even when multiple + // providers are configured. Drift is non-fatal: it must not block + // a brand-new provider in the same config from being inserted, + // which exercises the continue-after-drift path. cfg.Providers[1].Keys = []string{"sk-ant-2", "sk-ant-rotated"} - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") - require.Contains(t, err.Error(), `"primary-anthropic"`) + cfg.Providers = append(cfg.Providers, codersdk.AIProviderConfig{ + Type: "openai", + Name: "secondary-openai", + BaseURL: "https://api.openai.com/v1", + Keys: []string{"sk-new"}, + }) + ineffective, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) + // The non-drifting provider is still present with its original keys. oa, err := db.GetAIProviderByName(ctx, "primary-openai") require.NoError(t, err) oaKeys, err := db.GetAIProviderKeysByProviderID(ctx, oa.ID) require.NoError(t, err) require.ElementsMatch(t, []string{"sk-openai-1", "sk-openai-2"}, []string{oaKeys[0].APIKey, oaKeys[1].APIKey}) + // The drifted provider keeps its original keys; the env change + // was ignored. an, err := db.GetAIProviderByName(ctx, "primary-anthropic") require.NoError(t, err) anKeys, err := db.GetAIProviderKeysByProviderID(ctx, an.ID) require.NoError(t, err) require.ElementsMatch(t, []string{"sk-ant-1", "sk-ant-2"}, []string{anKeys[0].APIKey, anKeys[1].APIKey}) + + // The new provider was inserted despite the drift on another row. + sec, err := db.GetAIProviderByName(ctx, "secondary-openai") + require.NoError(t, err) + secKeys, err := db.GetAIProviderKeysByProviderID(ctx, sec.ID) + require.NoError(t, err) + require.Len(t, secKeys, 1) + require.Equal(t, "sk-new", secKeys[0].APIKey) }) t.Run("BedrockIndexedProviderHasNoKeys", func(t *testing.T) { @@ -367,7 +421,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "bedrock-anthropic") require.NoError(t, err) @@ -398,7 +453,9 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + // Pre-transaction validation: a legacy/indexed name conflict is + // still fatal. + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) require.Error(t, err) require.Contains(t, err.Error(), "conflicts") }) @@ -417,7 +474,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + // Pre-transaction validation: an invalid name is still fatal. + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) require.Error(t, err) require.Contains(t, err.Error(), "invalid AI provider name") }) @@ -446,7 +504,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) all, err := db.GetAIProviders(ctx, database.GetAIProvidersParams{}) require.NoError(t, err) @@ -465,7 +524,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Key: serpent.String("sk-original"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "openai") require.NoError(t, err) @@ -473,14 +533,15 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { // Re-run seed; the soft-deleted row should remain soft-deleted // and no new row should be created. - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) all, err := db.GetAIProviders(ctx, database.GetAIProvidersParams{}) require.NoError(t, err) require.Empty(t, all, "expected no active rows after soft-delete + re-seed") }) - t.Run("ExistingKeysBlockOnDrift", func(t *testing.T) { + t.Run("ExistingKeysDriftKeepsOriginalKeys", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitShort) @@ -491,17 +552,19 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { Key: serpent.String("sk-original"), }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) row, err := db.GetAIProviderByName(ctx, "openai") require.NoError(t, err) - // Operator rotates the env key. The seed now blocks startup - // because the keys differ, alerting the operator. + // Operator rotates the env key. The seed reports the change is + // ineffective rather than blocking startup, and the database row + // is left untouched. cfg.LegacyOpenAI.Key = serpent.String("sk-rotated") - err = coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) - require.Error(t, err) - require.Contains(t, err.Error(), "differs from the current environment configuration") + ineffective, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) + require.True(t, ineffective) // The original key is still in the database. keys, err := db.GetAIProviderKeysByProviderID(ctx, row.ID) @@ -533,7 +596,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) all, err := db.GetAIProviders(ctx, database.GetAIProvidersParams{}) require.NoError(t, err) @@ -563,7 +627,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + require.NoError(t, err) all, err := db.GetAIProviders(ctx, database.GetAIProvidersParams{}) require.NoError(t, err) @@ -579,7 +644,8 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitShort) - // Same name, different canonical fields: must be rejected. + // Same name, different canonical fields: pre-transaction + // validation rejects this, so it is still fatal. cfg := codersdk.AIBridgeConfig{ Providers: []codersdk.AIProviderConfig{ { @@ -596,7 +662,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) { }, }, } - err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) + _, err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)) require.Error(t, err) require.Contains(t, err.Error(), "conflicting fields") }) diff --git a/coderd/coderd.go b/coderd/coderd.go index c87adc5647..a08e95a444 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -2180,6 +2180,12 @@ type API struct { WebpushDispatcher webpush.Dispatcher QuotaCommitter atomic.Pointer[proto.QuotaCommitter] AppearanceFetcher atomic.Pointer[appearance.Fetcher] + // AIProvidersEnvDrift is set to true at startup when deprecated + // CODER_AIBRIDGE_* env configuration differs from the AI provider rows + // already stored in the database. The appearance fetchers read it to + // warn admins that their env changes are ineffective. The zero value + // (false) needs no initialization. + AIProvidersEnvDrift atomic.Bool // WorkspaceProxyHostsFn returns the hosts of healthy workspace proxies // for header reasons. WorkspaceProxyHostsFn atomic.Pointer[func() []*proxyhealth.ProxyHost] diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 37febd028b..eee7a52210 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -178,14 +178,19 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { //nolint:gocritic // Production timeout, not a test wait. aibridgeInitCtx, aibridgeInitCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) defer aibridgeInitCancel() - if err := agplcoderd.SeedAIProvidersFromEnv( + ineffective, err := agplcoderd.SeedAIProvidersFromEnv( aibridgeInitCtx, options.Database, options.DeploymentValues.AI.BridgeConfig, options.Logger.Named("aibridge.envseed"), - ); err != nil { + ) + if err != nil { return nil, nil, xerrors.Errorf("seed ai providers from env: %w", err) } + // Belt-and-suspenders: the agplcli re-seed after newAPI + // overwrites this with the same deterministic value. Use the + // computed flag rather than a hardcoded true. + api.AGPL.AIProvidersEnvDrift.Store(ineffective) aiBridgeProxyCloser, err := newAIBridgeProxyDaemon(api) if err != nil { _ = closers.Close()