From 56c9d0dcbb2fbce6a7ca5f33bc781f8c1954757c Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 1 Jun 2026 09:29:54 +0000 Subject: [PATCH] feat: expose ai_providers_env_drift_detected on appearance config Surface the deprecated-env-drift flag set at startup through a new read-only AppearanceConfig.ai_providers_env_drift_detected field. The AGPL and enterprise appearance fetchers read a process-local atomic (API.AIProvidersEnvDrift) so the dashboard can warn admins that their env changes are ineffective. Regenerated SDK types and swagger. --- coderd/apidoc/docs.go | 4 ++++ coderd/apidoc/swagger.json | 4 ++++ coderd/appearance/appearance.go | 17 ++++++++++++----- coderd/coderd.go | 2 +- codersdk/deployment.go | 5 +++++ docs/reference/api/enterprise.md | 1 + docs/reference/api/schemas.md | 18 ++++++++++-------- enterprise/coderd/appearance.go | 32 ++++++++++++++++++-------------- enterprise/coderd/coderd.go | 3 ++- site/site.go | 2 +- site/src/api/api.ts | 1 + site/src/api/typesGenerated.ts | 7 +++++++ site/src/testHelpers/entities.ts | 1 + 13 files changed, 67 insertions(+), 30 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1bbb216b1a..ee6e9c1b78 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15840,6 +15840,10 @@ const docTemplate = `{ "codersdk.AppearanceConfig": { "type": "object", "properties": { + "ai_providers_env_drift_detected": { + "description": "AIProvidersEnvDriftDetected is true when deprecated CODER_AIBRIDGE_*\nenv configuration differs from the AI provider rows already stored in\nthe database, meaning those env changes are ineffective. It is\noutput-only and is not part of UpdateAppearanceConfig.", + "type": "boolean" + }, "announcement_banners": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6f7224e972..926bcf934c 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14211,6 +14211,10 @@ "codersdk.AppearanceConfig": { "type": "object", "properties": { + "ai_providers_env_drift_detected": { + "description": "AIProvidersEnvDriftDetected is true when deprecated CODER_AIBRIDGE_*\nenv configuration differs from the AI provider rows already stored in\nthe database, meaning those env changes are ineffective. It is\noutput-only and is not part of UpdateAppearanceConfig.", + "type": "boolean" + }, "announcement_banners": { "type": "array", "items": { diff --git a/coderd/appearance/appearance.go b/coderd/appearance/appearance.go index f63cd77a59..34310d657f 100644 --- a/coderd/appearance/appearance.go +++ b/coderd/appearance/appearance.go @@ -2,6 +2,7 @@ package appearance import ( "context" + "sync/atomic" "github.com/coder/coder/v2/codersdk" ) @@ -12,21 +13,27 @@ type Fetcher interface { type AGPLFetcher struct { docsURL string + // aiProvidersEnvDrift reports whether deprecated CODER_AIBRIDGE_* env + // configuration is ineffective because it differs from the database. + // It may be nil when no source is wired in. + aiProvidersEnvDrift *atomic.Bool } func (f AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) { return codersdk.AppearanceConfig{ - AnnouncementBanners: []codersdk.BannerConfig{}, - SupportLinks: codersdk.DefaultSupportLinks(f.docsURL), - DocsURL: f.docsURL, + AnnouncementBanners: []codersdk.BannerConfig{}, + SupportLinks: codersdk.DefaultSupportLinks(f.docsURL), + DocsURL: f.docsURL, + AIProvidersEnvDriftDetected: f.aiProvidersEnvDrift != nil && f.aiProvidersEnvDrift.Load(), }, nil } -func NewDefaultFetcher(docsURL string) Fetcher { +func NewDefaultFetcher(docsURL string, aiProvidersEnvDrift *atomic.Bool) Fetcher { if docsURL == "" { docsURL = codersdk.DefaultDocsURL() } return &AGPLFetcher{ - docsURL: docsURL, + docsURL: docsURL, + aiProvidersEnvDrift: aiProvidersEnvDrift, } } diff --git a/coderd/coderd.go b/coderd/coderd.go index a08e95a444..7068ec7abe 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -666,7 +666,7 @@ func New(options *Options) *API { options.AppSigningKeyCache, ) - f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String()) + f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String(), &api.AIProvidersEnvDrift) api.AppearanceFetcher.Store(&f) api.PortSharer.Store(&portsharing.DefaultPortSharer) api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 3fb36c587f..a49c190f0a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -4882,6 +4882,11 @@ type AppearanceConfig struct { ServiceBanner BannerConfig `json:"service_banner"` AnnouncementBanners []BannerConfig `json:"announcement_banners"` SupportLinks []LinkConfig `json:"support_links,omitempty"` + // AIProvidersEnvDriftDetected is true when deprecated CODER_AIBRIDGE_* + // env configuration differs from the AI provider rows already stored in + // the database, meaning those env changes are ineffective. It is + // output-only and is not part of UpdateAppearanceConfig. + AIProvidersEnvDriftDetected bool `json:"ai_providers_env_drift_detected"` } type UpdateAppearanceConfig struct { diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index ed1ce268e7..fe04967e6c 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -103,6 +103,7 @@ curl -X GET http://coder-server:8080/api/v2/appearance \ ```json { + "ai_providers_env_drift_detected": true, "announcement_banners": [ { "background_color": "string", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ea8f19c4bf..e011408fe1 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1572,6 +1572,7 @@ None ```json { + "ai_providers_env_drift_detected": true, "announcement_banners": [ { "background_color": "string", @@ -1600,14 +1601,15 @@ None ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------|---------------------------------------------------------|----------|--------------|---------------------------------------------------------------------| -| `announcement_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false | | | -| `application_name` | string | false | | | -| `docs_url` | string | false | | | -| `logo_url` | string | false | | | -| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by AnnouncementBanners. | -| `support_links` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------------|---------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ai_providers_env_drift_detected` | boolean | false | | Ai providers env drift detected is true when deprecated CODER_AIBRIDGE_* env configuration differs from the AI provider rows already stored in the database, meaning those env changes are ineffective. It is output-only and is not part of UpdateAppearanceConfig. | +| `announcement_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false | | | +| `application_name` | string | false | | | +| `docs_url` | string | false | | | +| `logo_url` | string | false | | | +| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by AnnouncementBanners. | +| `support_links` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | | ## codersdk.ArchiveTemplateVersionsRequest diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go index db845fadea..9e3ceb6e83 100644 --- a/enterprise/coderd/appearance.go +++ b/enterprise/coderd/appearance.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "sync/atomic" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -42,21 +43,23 @@ func (api *API) appearance(rw http.ResponseWriter, r *http.Request) { } type appearanceFetcher struct { - database database.Store - supportLinks []codersdk.LinkConfig - docsURL string - coderVersion string + database database.Store + supportLinks []codersdk.LinkConfig + docsURL string + coderVersion string + aiProvidersEnvDrift *atomic.Bool } -func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig, docsURL, coderVersion string) agpl.Fetcher { +func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig, docsURL, coderVersion string, aiProvidersEnvDrift *atomic.Bool) agpl.Fetcher { if docsURL == "" { docsURL = codersdk.DefaultDocsURL() } return &appearanceFetcher{ - database: store, - supportLinks: links, - docsURL: docsURL, - coderVersion: coderVersion, + database: store, + supportLinks: links, + docsURL: docsURL, + coderVersion: coderVersion, + aiProvidersEnvDrift: aiProvidersEnvDrift, } } @@ -94,11 +97,12 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi } cfg := codersdk.AppearanceConfig{ - ApplicationName: applicationName, - LogoURL: logoURL, - AnnouncementBanners: []codersdk.BannerConfig{}, - SupportLinks: codersdk.DefaultSupportLinks(f.docsURL), - DocsURL: f.docsURL, + ApplicationName: applicationName, + LogoURL: logoURL, + AnnouncementBanners: []codersdk.BannerConfig{}, + SupportLinks: codersdk.DefaultSupportLinks(f.docsURL), + DocsURL: f.docsURL, + AIProvidersEnvDriftDetected: f.aiProvidersEnvDrift != nil && f.aiProvidersEnvDrift.Load(), } if announcementBannersJSON != "" { diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 33732eea3d..ab1b059428 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -1014,10 +1014,11 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.DeploymentValues.Support.Links.Value, api.DeploymentValues.DocsURL.String(), buildinfo.Version(), + &api.AGPL.AIProvidersEnvDrift, ) api.AGPL.AppearanceFetcher.Store(&f) } else { - f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String()) + f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String(), &api.AGPL.AIProvidersEnvDrift) api.AGPL.AppearanceFetcher.Store(&f) } } diff --git a/site/site.go b/site/site.go index 9ba43f79f9..f5d290a35b 100644 --- a/site/site.go +++ b/site/site.go @@ -88,7 +88,7 @@ type Options struct { func New(opts *Options) (*Handler, error) { if opts.AppearanceFetcher == nil { daf := atomic.Pointer[appearance.Fetcher]{} - f := appearance.NewDefaultFetcher(opts.DocsURL) + f := appearance.NewDefaultFetcher(opts.DocsURL, nil) daf.Store(&f) opts.AppearanceFetcher = &daf } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8976bf901c..d87791166f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2393,6 +2393,7 @@ class ApiMethods { service_banner: { enabled: false, }, + ai_providers_env_drift_detected: false, }; } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c4fae7a3d0..09aac68ef6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1132,6 +1132,13 @@ export interface AppearanceConfig { readonly service_banner: BannerConfig; readonly announcement_banners: readonly BannerConfig[]; readonly support_links?: readonly LinkConfig[]; + /** + * AIProvidersEnvDriftDetected is true when deprecated CODER_AIBRIDGE_* + * env configuration differs from the AI provider rows already stored in + * the database, meaning those env changes are ineffective. It is + * output-only and is not part of UpdateAppearanceConfig. + */ + readonly ai_providers_env_drift_detected: boolean; } // From codersdk/templates.go diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 07bd064129..095c893d15 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3392,6 +3392,7 @@ export const MockAppearanceConfig: TypesGen.AppearanceConfig = { }, announcement_banners: [], docs_url: "https://coder.com/docs/@main/", + ai_providers_env_drift_detected: false, }; export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = {