Ctrl K
ComunidadGuías…

coderd/coderd.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd

import (
	"context"
	"crypto/tls"
	"database/sql"
	_ "embed"
	"errors"
	"expvar"
	"flag"
	"fmt"
	"io"
	"math"
	"net/http"
	httppprof "net/http/pprof"
	"net/netip"
	"net/url"
	"path/filepath"
	"regexp"
	"runtime/pprof"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	httpSwagger "github.com/swaggo/http-swagger/v2"
	"go.opentelemetry.io/otel/trace"
	"golang.org/x/xerrors"
	"google.golang.org/api/idtoken"
	"storj.io/drpc/drpcmux"
	"storj.io/drpc/drpcserver"
	"tailscale.com/derp"
	"tailscale.com/derp/derphttp"
	"tailscale.com/tailcfg"
	"tailscale.com/types/key"
	"tailscale.com/util/singleflight"

	"cdr.dev/slog/v3"
	agentproto "github.com/coder/coder/v2/agent/proto"
	"github.com/coder/coder/v2/buildinfo"
	"github.com/coder/coder/v2/coderd/agentapi"
	"github.com/coder/coder/v2/coderd/agentapi/metadatabatcher"
	"github.com/coder/coder/v2/coderd/aibridge"
	"github.com/coder/coder/v2/coderd/aibridge/prices"
	"github.com/coder/coder/v2/coderd/aibridgedserver"
	"github.com/coder/coder/v2/coderd/aiseats"
	_ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs.
	"github.com/coder/coder/v2/coderd/appearance"
	"github.com/coder/coder/v2/coderd/audit"
	"github.com/coder/coder/v2/coderd/awsidentity"
	"github.com/coder/coder/v2/coderd/azureidentity"
	"github.com/coder/coder/v2/coderd/boundaryusage"
	"github.com/coder/coder/v2/coderd/connectionlog"
	"github.com/coder/coder/v2/coderd/cryptokeys"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbrollup"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/database/pubsub"
	"github.com/coder/coder/v2/coderd/entitlements"
	experimentrules "github.com/coder/coder/v2/coderd/experiments"
	"github.com/coder/coder/v2/coderd/externalauth"
	"github.com/coder/coder/v2/coderd/files"
	"github.com/coder/coder/v2/coderd/gitsshkey"
	"github.com/coder/coder/v2/coderd/healthcheck"
	"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
	"github.com/coder/coder/v2/coderd/httpapi"
	"github.com/coder/coder/v2/coderd/httpmw"
	"github.com/coder/coder/v2/coderd/httpmw/loggermw"
	"github.com/coder/coder/v2/coderd/idpsync"
	"github.com/coder/coder/v2/coderd/mcp"
	"github.com/coder/coder/v2/coderd/metricscache"
	"github.com/coder/coder/v2/coderd/notifications"
	"github.com/coder/coder/v2/coderd/oauth2provider"
	"github.com/coder/coder/v2/coderd/portsharing"
	"github.com/coder/coder/v2/coderd/pproflabel"
	"github.com/coder/coder/v2/coderd/prebuilds"
	"github.com/coder/coder/v2/coderd/prometheusmetrics"
	"github.com/coder/coder/v2/coderd/provisionerdserver"
	"github.com/coder/coder/v2/coderd/proxyhealth"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/rbac/policy"
	"github.com/coder/coder/v2/coderd/rbac/rolestore"
	"github.com/coder/coder/v2/coderd/runtimeconfig"
	"github.com/coder/coder/v2/coderd/schedule"
	"github.com/coder/coder/v2/coderd/telemetry"
	"github.com/coder/coder/v2/coderd/tracing"
	"github.com/coder/coder/v2/coderd/updatecheck"
	"github.com/coder/coder/v2/coderd/usage"
	"github.com/coder/coder/v2/coderd/util/slice"
	"github.com/coder/coder/v2/coderd/webpush"
	"github.com/coder/coder/v2/coderd/workspaceapps"
	"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
	"github.com/coder/coder/v2/coderd/workspaceconnwatcher"
	"github.com/coder/coder/v2/coderd/workspacestats"
	"github.com/coder/coder/v2/coderd/wsbuilder"
	"github.com/coder/coder/v2/coderd/wsbuildorchestrator"
	"github.com/coder/coder/v2/coderd/x/agenthooks/dispatch"
	"github.com/coder/coder/v2/coderd/x/chatd"
	"github.com/coder/coder/v2/coderd/x/chatd/chatloop"
	"github.com/coder/coder/v2/coderd/x/chatd/mcpclient"
	"github.com/coder/coder/v2/coderd/x/gitsync"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/codersdk/drpcsdk"
	"github.com/coder/coder/v2/codersdk/healthsdk"
	sharedhttpmw "github.com/coder/coder/v2/httpmw"
	"github.com/coder/coder/v2/provisionerd/proto"
	"github.com/coder/coder/v2/provisionersdk"
	"github.com/coder/coder/v2/site"
	"github.com/coder/coder/v2/tailnet"
	"github.com/coder/coder/v2/tailnet/derpmetrics"
	"github.com/coder/quartz"
	"github.com/coder/safedial"
	"github.com/coder/serpent"
)

// We must only ever instantiate one httpSwagger.Handler because of a data race
// inside the handler. This issue is triggered by tests that create multiple
// coderd instances.
//
// See https://github.com/swaggo/http-swagger/issues/78
var globalHTTPSwaggerHandler http.HandlerFunc

//go:embed swagger_request_interceptor.js
var swaggerRequestInterceptor string

func init() {
	globalHTTPSwaggerHandler = httpSwagger.Handler(
		httpSwagger.URL("/swagger/doc.json"),
		// The swagger UI has an "Authorize" button that will input the
		// credentials into the Coder-Session-Token header. This bypasses
		// CSRF checks **if** there is no cookie auth also present.
		// (If the cookie matches, then it's ok too)
		//
		// Because swagger is hosted on the same domain, we have the cookie
		// auth and the header auth competing. This can cause CSRF errors,
		// and can be confusing what authentication is being used.
		//
		// So remove authenticating via a cookie, and rely on the authorization
		// header passed in.
		httpSwagger.UIConfig(map[string]string{
			// The interceptor source lives in swagger_request_interceptor.js so
			// it can be edited as real JavaScript.
			// See https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/.
			"requestInterceptor": swaggerRequestInterceptor,
			"withCredentials":    "false",
		}))
}

var expDERPOnce = sync.Once{}

// Options are requires parameters for Coder to start.
type Options struct {
	AccessURL *url.URL
	// AppHostname should be the wildcard hostname to use for workspace
	// applications INCLUDING the asterisk, (optional) suffix and leading dot.
	// It will use the same scheme and port number as the access URL.
	// E.g. "*.apps.coder.com" or "*-apps.coder.com" or "*.apps.coder.com:8080".
	AppHostname string
	// AppHostnameRegex contains the regex version of options.AppHostname as
	// generated by appurl.CompileHostnamePattern(). It MUST be set if
	// options.AppHostname is set.
	AppHostnameRegex *regexp.Regexp
	Logger           slog.Logger
	Database         database.Store
	Pubsub           pubsub.Pubsub
	// ReplicaSyncPubsub is used explicitly to instantiate the replicasync manager downstream if it exists.
	// All other consumers of pubsub should reference Options.Pubsub.
	ReplicaSyncPubsub pubsub.Pubsub
	RuntimeConfig     *runtimeconfig.Manager

	// CacheDir is used for caching files served by the API.
	CacheDir string

	Auditor                        audit.Auditor
	ConnectionLogger               connectionlog.ConnectionLogger
	AgentConnectionUpdateFrequency time.Duration
	AgentInactiveDisconnectTimeout time.Duration
	AWSCertificates                awsidentity.Certificates
	Authorizer                     rbac.Authorizer
	AzureCertificates              azureidentity.Options
	GoogleTokenValidator           *idtoken.Validator
	GithubOAuth2Config             *GithubOAuth2Config
	OIDCConfig                     *OIDCConfig
	PrometheusRegistry             *prometheus.Registry
	StrictTransportSecurityCfg     httpmw.HSTSConfig
	SSHKeygenAlgorithm             gitsshkey.Algorithm
	Telemetry                      telemetry.Reporter
	TracerProvider                 trace.TracerProvider
	ExternalAuthConfigs            []*externalauth.Config
	RealIPConfig                   *httpmw.RealIPConfig
	TrialGenerator                 func(ctx context.Context, body codersdk.LicensorTrialRequest) error
	// RefreshEntitlements is used to set correct entitlements after creating first user and generating trial license.
	RefreshEntitlements func(ctx context.Context) error
	// Entitlements can come from the enterprise caller if enterprise code is
	// included.
	Entitlements *entitlements.Set
	// PostAuthAdditionalHeadersFunc is used to add additional headers to the response
	// after a successful authentication.
	// This is somewhat janky, but seemingly the only reasonable way to add a header
	// for all authenticated users under a condition, only in Enterprise.
	PostAuthAdditionalHeadersFunc func(auth rbac.Subject, header http.Header)

	// TLSCertificates is used to mesh DERP servers securely.
	TLSCertificates    []tls.Certificate
	TailnetCoordinator tailnet.Coordinator
	DERPServer         *derp.Server
	// ClusterHost is this replica's routable cluster address (IP or hostname),
	// resolved from DeploymentValues.Cluster.Host, falling back to the DERP
	// relay host for older HA deployments that predate the setting. It is used
	// as the NATS cluster route host and, when it is an IP, the cluster mTLS
	// leaf IP SAN. It is consumed by the NATS pubsub (AGPL) and, under
	// enterprise HA, by replicasync.
	ClusterHost string
	// BaseDERPMap is used as the base DERP map for all clients and agents.
	// Proxies are added to this list.
	BaseDERPMap                    *tailcfg.DERPMap
	DERPMapUpdateFrequency         time.Duration
	NetworkTelemetryBatchFrequency time.Duration
	NetworkTelemetryBatchMaxSize   int
	SwaggerEndpoint                bool
	TemplateScheduleStore          *atomic.Pointer[schedule.TemplateScheduleStore]
	UserQuietHoursScheduleStore    *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
	AccessControlStore             *atomic.Pointer[dbauthz.AccessControlStore]
	UsageInserter                  *atomic.Pointer[usage.Inserter]
	// CoordinatorResumeTokenProvider is used to provide and validate resume
	// tokens issued by and passed to the coordinator DRPC API.
	CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider

	HealthcheckFunc              func(ctx context.Context, apiKey string, progress *healthcheck.Progress) *healthsdk.HealthcheckReport
	HealthcheckTimeout           time.Duration
	HealthcheckRefresh           time.Duration
	WorkspaceProxiesFetchUpdater *atomic.Pointer[healthcheck.WorkspaceProxiesFetchUpdater]

	// OAuthSigningKey is the crypto key used to sign and encrypt state strings
	// related to OAuth. This is a symmetric secret key using hmac to sign payloads.
	// So this secret should **never** be exposed to the client.
	OAuthSigningKey [32]byte

	// APIRateLimit is the minutely throughput rate limit per user or ip.
	// Setting a rate limit <0 will disable the rate limiter across the entire
	// app. Some specific routes have their own configurable rate limits.
	APIRateLimit   int
	LoginRateLimit int
	FilesRateLimit int

	MetricsCacheRefreshInterval time.Duration
	AgentStatsRefreshInterval   time.Duration
	DeploymentValues            *codersdk.DeploymentValues
	// DeploymentOptions do contain the copy of DeploymentValues, and contain
	// contextual information about how the values were set.
	// Do not use DeploymentOptions to retrieve values, use DeploymentValues instead.
	// All secrets values are stripped.
	DeploymentOptions  serpent.OptionSet
	UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking.

	// SSHConfig is the response clients use to configure config-ssh locally.
	SSHConfig codersdk.SSHConfigResponse

	HTTPClient *http.Client
	// MCPAllowedPrivateCIDRs exempts IP ranges from the SSRF guard for
	// MCP server, OAuth2 discovery, token, and revocation traffic.
	MCPAllowedPrivateCIDRs []netip.Prefix
	// ChatStreamPartsDialer dials remote chat stream parts.
	// Set by enterprise for HA deployments. Nil uses chatd's local
	// in-process channel dialer.
	ChatStreamPartsDialer chatd.StreamPartsDialer
	// Nil keeps the default chat agent caps active.
	ChatAgentCapacityUnlock chatd.AgentCapacityUnlock
	// ChatInternalMCPServers supplies in-process MCP servers for chats.
	// Servers must be ready when New starts the chat worker.
	ChatInternalMCPServers []chatd.InternalMCPServer
	// ChatWorkerDisabled skips starting the chat daemon's background
	// worker.
	ChatWorkerDisabled bool

	UpdateAgentMetrics func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric)
	StatsBatcher       workspacestats.Batcher

	MetadataBatcherOptions []metadatabatcher.Option

	ProvisionerdServerMetrics *provisionerdserver.Metrics
	WorkspaceBuilderMetrics   *wsbuilder.Metrics

	// WorkspaceAppAuditSessionTimeout allows changing the timeout for audit
	// sessions. Raising or lowering this value will directly affect the write
	// load of the audit log table. This is used for testing. Default 1 hour.
	WorkspaceAppAuditSessionTimeout    time.Duration
	WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions

	// This janky function is used in telemetry to parse fields out of the raw
	// JWT. It needs to be passed through like this because license parsing is
	// under the enterprise license, and can't be imported into AGPL.
	ParseLicenseClaims    func(rawJWT string) (email string, trial bool, err error)
	AllowWorkspaceRenames bool

	// NewTicker is used for unit tests to replace "time.NewTicker".
	NewTicker func(duration time.Duration) (tick <-chan time.Time, done func())

	// DatabaseRolluper rolls up template usage stats from raw agent and app
	// stats. This is used to provide insights in the WebUI.
	DatabaseRolluper *dbrollup.Rolluper
	// WorkspaceUsageTracker tracks workspace usage by the CLI.
	WorkspaceUsageTracker *workspacestats.UsageTracker
	// BoundaryUsageTracker tracks boundary usage for telemetry.
	BoundaryUsageTracker *boundaryusage.Tracker
	// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
	NotificationsEnqueuer notifications.Enqueuer

	// IDPSync holds all configured values for syncing external IDP users into Coder.
	IDPSync idpsync.IDPSync

	// OneTimePasscodeValidityPeriod specifies how long a one time passcode should be valid for.
	OneTimePasscodeValidityPeriod time.Duration

	// Keycaches
	AppSigningKeyCache    cryptokeys.SigningKeycache
	AppEncryptionKeyCache cryptokeys.EncryptionKeycache
	OIDCConvertKeyCache   cryptokeys.SigningKeycache
	ChatFileTokenKeyCache cryptokeys.SigningKeycache
	// NATSCACache serves the NATS cluster mTLS CA via the generic signing key
	// cache for the nats_ca feature. SigningKey returns the active CA
	// (a *NATSCA); VerifyingKey returns a specific CA by sequence. The key
	// rotator is the sole creator of nats_ca rows, so this cache is read-only.
	NATSCACache cryptokeys.SigningKeycache
	Clock       quartz.Clock
	// Acquirer acquires provisioner jobs. Defaults to provisionerdserver.Acquirer
	// backed by Database and Pubsub.
	Acquirer *provisionerdserver.Acquirer

	// WebPushDispatcher is a way to send notifications over Web Push.
	WebPushDispatcher webpush.Dispatcher
}

// @title Coder API
// @version 2.0
// @description Coderd is the service created by running coder server. It is a thin API that connects workspaces, provisioners and users. coderd stores its state in Postgres and is the only service that communicates with Postgres.
// @termsOfService https://coder.com/legal/terms-of-service

// @contact.name API Support
// @contact.url https://coder.com
// @contact.email support@coder.com

// @license.name AGPL-3.0
// @license.url https://github.com/coder/coder/blob/main/LICENSE

// @BasePath /

// @tag.name Agents
// @tag.description Workspace agent endpoints. These power the workspace agent daemon defined by the `coder_agent` Terraform resource. This API is NOT the Coder Agents Chats API. For programmatic access to AI Coder Agents, see the Chats API.

// @tag.name Chats
// @tag.description Programmatic API for Coder Agents (the user-facing "Coder Agents" / "Chats" product). Use these endpoints to create, list, and manage AI coding agent sessions.

// @securitydefinitions.apiKey Authorization
// @in header
// @name Authorization

// @securitydefinitions.apiKey CoderSessionToken
// @in header
// @name Coder-Session-Token

// @securitydefinitions.apiKey AIGatewayKey
// @in header
// @name X-AI-Governance-Gateway-Key
// New constructs a Coder API handler.
func New(options *Options) *API {
	if options == nil {
		options = &Options{}
	}
	if options.Entitlements == nil {
		options.Entitlements = entitlements.New()
	}
	if options.NewTicker == nil {
		options.NewTicker = func(duration time.Duration) (tick <-chan time.Time, done func()) {
			ticker := time.NewTicker(duration)
			return ticker.C, ticker.Stop
		}
	}

	// Safety check: if we're not running a unit test, we *must* have a Prometheus registry.
	if options.PrometheusRegistry == nil && flag.Lookup("test.v") == nil {
		panic("developer error: options.PrometheusRegistry is nil and not running a unit test")
	}

	experiments := ReadExperiments(
		options.Logger, options.DeploymentValues.Experiments.Value(),
	)

	// Only reload when an option deviates from the defaults so the zero
	// value keeps the stock builtin roles. Constructing the full options
	// struct here (rather than mirroring individual fields in the guard)
	// ensures a newly added RoleOptions field cannot be silently ignored.
	roleOptions := rbac.RoleOptions{
		NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
		NoWorkspaceSharing:   bool(options.DeploymentValues.DisableWorkspaceSharing),
		NoChatSharing:        bool(options.DeploymentValues.DisableChatSharing),
	}
	if roleOptions != (rbac.RoleOptions{}) {
		rbac.ReloadBuiltinRoles(&roleOptions)
	}

	if options.DeploymentValues.DisableWorkspaceSharing {
		rbac.SetWorkspaceACLDisabled(true)
	}
	if options.DeploymentValues.DisableChatSharing {
		rbac.SetChatACLDisabled(true)
	}

	if options.PrometheusRegistry == nil {
		options.PrometheusRegistry = prometheus.NewRegistry()
		options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
	}
	if options.Authorizer == nil {
		options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
		if buildinfo.IsDev() {
			options.Authorizer = rbac.Recorder(options.Authorizer)
		}
	}

	if options.AccessControlStore == nil {
		options.AccessControlStore = &atomic.Pointer[dbauthz.AccessControlStore]{}
		var tacs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
		options.AccessControlStore.Store(&tacs)
	}

	options.Database = dbauthz.New(
		options.Database,
		options.Authorizer,
		options.Logger.Named("authz_querier"),
		options.AccessControlStore,
	)

	if options.IDPSync == nil {
		options.IDPSync = idpsync.NewAGPLSync(options.Logger, options.RuntimeConfig, idpsync.FromDeploymentValues(options.DeploymentValues))
	}

	if options.AppHostname != "" && options.AppHostnameRegex == nil || options.AppHostname == "" && options.AppHostnameRegex != nil {
		panic("coderd: both AppHostname and AppHostnameRegex must be set or unset")
	}
	if options.AgentConnectionUpdateFrequency == 0 {
		options.AgentConnectionUpdateFrequency = 15 * time.Second
	}
	if options.AgentInactiveDisconnectTimeout == 0 {
		// Multiply the update by two to allow for some lag-time.
		options.AgentInactiveDisconnectTimeout = options.AgentConnectionUpdateFrequency * 2
		// Set a minimum timeout to avoid disconnecting too soon.
		if options.AgentInactiveDisconnectTimeout < 2*time.Second {
			options.AgentInactiveDisconnectTimeout = 2 * time.Second
		}
	}
	if options.AgentStatsRefreshInterval == 0 {
		options.AgentStatsRefreshInterval = 5 * time.Minute
	}
	if options.MetricsCacheRefreshInterval == 0 {
		options.MetricsCacheRefreshInterval = time.Hour
	}
	if options.APIRateLimit == 0 {
		options.APIRateLimit = 512
	}
	if options.LoginRateLimit == 0 {
		options.LoginRateLimit = 60
	}
	if options.FilesRateLimit == 0 {
		options.FilesRateLimit = 12
	}
	if options.Clock == nil {
		options.Clock = quartz.NewReal()
	}
	if options.DERPServer == nil && options.DeploymentValues.DERP.Server.Enable {
		options.DERPServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp")))
	}
	if options.DERPMapUpdateFrequency == 0 {
		options.DERPMapUpdateFrequency = 5 * time.Second
	}
	if options.NetworkTelemetryBatchFrequency == 0 {
		options.NetworkTelemetryBatchFrequency = 1 * time.Minute
	}
	if options.NetworkTelemetryBatchMaxSize == 0 {
		options.NetworkTelemetryBatchMaxSize = 1_000
	}
	if options.TailnetCoordinator == nil {
		options.TailnetCoordinator = tailnet.NewCoordinator(options.Logger)
	}
	if options.Auditor == nil {
		options.Auditor = audit.NewNop()
	}
	if options.ConnectionLogger == nil {
		options.ConnectionLogger = connectionlog.NewNop()
	}
	if options.SSHConfig.HostnamePrefix == "" {
		options.SSHConfig.HostnamePrefix = "coder."
	}
	if options.TracerProvider == nil {
		options.TracerProvider = trace.NewNoopTracerProvider()
	}
	if options.TemplateScheduleStore == nil {
		options.TemplateScheduleStore = &atomic.Pointer[schedule.TemplateScheduleStore]{}
	}
	if options.TemplateScheduleStore.Load() == nil {
		v := schedule.NewAGPLTemplateScheduleStore()
		options.TemplateScheduleStore.Store(&v)
	}
	if options.UserQuietHoursScheduleStore == nil {
		options.UserQuietHoursScheduleStore = &atomic.Pointer[schedule.UserQuietHoursScheduleStore]{}
	}
	if options.UserQuietHoursScheduleStore.Load() == nil {
		v := schedule.NewAGPLUserQuietHoursScheduleStore()
		options.UserQuietHoursScheduleStore.Store(&v)
	}
	if options.UsageInserter == nil {
		options.UsageInserter = &atomic.Pointer[usage.Inserter]{}
	}
	if options.UsageInserter.Load() == nil {
		inserter := usage.NewAGPLInserter()
		options.UsageInserter.Store(&inserter)
	}
	if options.OneTimePasscodeValidityPeriod == 0 {
		options.OneTimePasscodeValidityPeriod = 20 * time.Minute
	}

	if options.StatsBatcher == nil {
		panic("developer error: options.StatsBatcher is nil")
	}

	siteCacheDir := options.CacheDir
	if siteCacheDir != "" {
		siteCacheDir = filepath.Join(siteCacheDir, "site")
	}

	metricsCache := metricscache.New(
		options.Database,
		options.Logger.Named("metrics_cache"),
		quartz.NewReal(),
		metricscache.Intervals{
			TemplateBuildTimes: options.MetricsCacheRefreshInterval,
			DeploymentStats:    options.AgentStatsRefreshInterval,
		},
		experiments.Enabled(codersdk.ExperimentWorkspaceUsage),
	)

	oauthConfigs := &httpmw.OAuth2Configs{
		Github: options.GithubOAuth2Config,
		OIDC:   options.OIDCConfig,
	}

	if options.DatabaseRolluper == nil {
		options.DatabaseRolluper = dbrollup.New(options.Logger.Named("dbrollup"), options.Database)
	}

	if options.WorkspaceUsageTracker == nil {
		options.WorkspaceUsageTracker = workspacestats.NewTracker(options.Database,
			workspacestats.TrackerWithLogger(options.Logger.Named("workspace_usage_tracker")),
		)
	}

	if options.NotificationsEnqueuer == nil {
		options.NotificationsEnqueuer = notifications.NewNoopEnqueuer()
	}

	r := chi.NewRouter()
	// We add this middleware early, to make sure that authorization checks made
	// by other middleware get recorded.
	if buildinfo.IsDev() {
		r.Use(httpmw.RecordAuthzChecks(options.DeploymentValues.EnableAuthzRecording.Value()))
	}

	ctx, cancel := context.WithCancel(context.Background())

	// nolint:gocritic // Load deployment ID. This never changes
	depID, err := options.Database.GetDeploymentID(dbauthz.AsSystemRestricted(ctx))
	if err != nil {
		panic(xerrors.Errorf("get deployment ID: %w", err))
	}

	fetcher := &cryptokeys.DBFetcher{
		DB: options.Database,
	}

	if options.OIDCConvertKeyCache == nil {
		options.OIDCConvertKeyCache, err = cryptokeys.NewSigningCache(ctx,
			options.Logger.Named("oidc_convert_keycache"),
			fetcher,
			codersdk.CryptoKeyFeatureOIDCConvert,
		)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to properly instantiate oidc convert signing cache", slog.Error(err))
		}
	}

	if options.ChatFileTokenKeyCache == nil {
		options.ChatFileTokenKeyCache, err = cryptokeys.NewSigningCache(ctx,
			options.Logger.Named("chat_file_token_keycache"),
			fetcher,
			codersdk.CryptoKeyFeatureChatFilesToken,
		)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to properly instantiate chat file token signing cache", slog.Error(err))
		}
	}

	if options.AppSigningKeyCache == nil {
		options.AppSigningKeyCache, err = cryptokeys.NewSigningCache(ctx,
			options.Logger.Named("app_signing_keycache"),
			fetcher,
			codersdk.CryptoKeyFeatureWorkspaceAppsToken,
		)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to properly instantiate app signing key cache", slog.Error(err))
		}
	}

	if options.AppEncryptionKeyCache == nil {
		options.AppEncryptionKeyCache, err = cryptokeys.NewEncryptionCache(ctx,
			options.Logger,
			fetcher,
			codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey,
		)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to properly instantiate app encryption key cache", slog.Error(err))
		}
	}

	if options.CoordinatorResumeTokenProvider == nil {
		fetcher := &cryptokeys.DBFetcher{
			DB: options.Database,
		}

		resumeKeycache, err := cryptokeys.NewSigningCache(ctx,
			options.Logger,
			fetcher,
			codersdk.CryptoKeyFeatureTailnetResume,
		)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to properly instantiate tailnet resume signing cache", slog.Error(err))
		}
		options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(
			resumeKeycache,
			options.Clock,
			tailnet.DefaultResumeTokenExpiry,
		)
	}

	updatesProvider := NewUpdatesProvider(options.Logger.Named("workspace_updates"), options.Pubsub, options.Database, options.Authorizer)

	// Start a background process that rotates keys. We intentionally start this after the caches
	// are created to force initial requests for a key to populate the caches. This helps catch
	// bugs that may only occur when a key isn't precached in tests and the latency cost is minimal.
	cryptokeys.StartRotator(ctx, options.Logger, options.Database)

	// The NATS CA cache is read-only and depends on the rotator having minted
	// the nats_ca CA, so it must be constructed after StartRotator.
	if options.NATSCACache == nil {
		options.NATSCACache, err = cryptokeys.NewSigningCache(ctx, options.Logger.Named("nats_ca_cache"), &cryptokeys.DBFetcher{DB: options.Database}, codersdk.CryptoKeyFeatureNATSCA)
		if err != nil {
			options.Logger.Fatal(ctx, "failed to instantiate NATS CA cache", slog.Error(err))
		}
	}

	// Ensure all system role permissions are current.
	//nolint:gocritic // Startup reconciliation reads/writes system roles. There is
	// no user request context here, so use a system-restricted context.
	err = rolestore.ReconcileSystemRoles(dbauthz.AsSystemRestricted(ctx), options.Logger, options.Database)
	if err != nil {
		// Not ideal, but not using Fatal here and just continuing
		// after logging the error would be a potential security hole.
		options.Logger.Fatal(ctx, "failed to reconcile system role permissions", slog.Error(err))
	}

	// Seed the AI Bridge model price table from the embedded price book.
	//nolint:gocritic // Startup seeder needs to run as aibridge context.
	if err := prices.Seed(dbauthz.AsAIBridged(ctx), options.Database); err != nil {
		options.Logger.Error(ctx, "failed to seed AI Gateway prices; cost tracking may use stale prices", slog.Error(err))
	}

	// AGPL uses a no-op build usage checker as there are no license
	// entitlements to enforce. This is swapped out in
	// enterprise/coderd/coderd.go.
	var buildUsageChecker atomic.Pointer[wsbuilder.UsageChecker]
	var noopUsageChecker wsbuilder.UsageChecker = wsbuilder.NoopUsageChecker{}
	buildUsageChecker.Store(&noopUsageChecker)
	acquirer := options.Acquirer
	if acquirer == nil {
		acquirer = provisionerdserver.NewAcquirer(
			ctx,
			options.Logger.Named("acquirer"),
			options.Database,
			options.Pubsub,
		)
	}
	mcpHTTPClient := mcpclient.NewHTTPClient(
		options.HTTPClient,
		safedial.WithAllowedPrefixes(options.MCPAllowedPrivateCIDRs...),
	)
	api := &API{
		ctx:           ctx,
		cancel:        cancel,
		DeploymentID:  depID,
		ID:            uuid.New(),
		Options:       options,
		mcpHTTPClient: mcpHTTPClient,
		RootHandler:   r,
		HTTPAuth: &HTTPAuthorizer{
			Authorizer: options.Authorizer,
			Logger:     options.Logger,
		},
		metricsCache:                metricsCache,
		Auditor:                     atomic.Pointer[audit.Auditor]{},
		ConnectionLogger:            atomic.Pointer[connectionlog.ConnectionLogger]{},
		TailnetCoordinator:          atomic.Pointer[tailnet.Coordinator]{},
		UpdatesProvider:             updatesProvider,
		TemplateScheduleStore:       options.TemplateScheduleStore,
		UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
		AccessControlStore:          options.AccessControlStore,
		BuildUsageChecker:           &buildUsageChecker,
		UsageInserter:               options.UsageInserter,
		FileCache:                   files.New(options.PrometheusRegistry, options.Authorizer),
		Experiments:                 experiments,
		WebpushDispatcher:           options.WebPushDispatcher,
		healthCheckGroup:            &singleflight.Group[string, *healthsdk.HealthcheckReport]{},
		Acquirer:                    acquirer,
		dbRolluper:                  options.DatabaseRolluper,
		ProfileCollector:            defaultProfileCollector{},
		AISeatTracker:               aiseats.Noop{},
	}

	api.ExperimentEvaluator, err = experimentrules.New(
		options.Logger.Named("experiments"),
		experimentrules.NewDBStore(options.Database),
		experiments,
	)
	if err != nil {
		panic(xerrors.Errorf("create experiment evaluator: %w", err))
	}

	api.WorkspaceAppsProvider = workspaceapps.NewDBTokenProvider(
		ctx,
		options.Logger.Named("workspaceapps"),
		options.AccessURL,
		options.Authorizer,
		&api.ConnectionLogger,
		options.Database,
		options.DeploymentValues,
		oauthConfigs,
		options.AgentInactiveDisconnectTimeout,
		options.WorkspaceAppAuditSessionTimeout,
		options.AppSigningKeyCache,
		options.PrometheusRegistry,
	)

	f := appearance.NewDefaultFetcher(options.Database, api.DeploymentValues.DocsURL.String())
	api.AppearanceFetcher.Store(&f)
	api.PortSharer.Store(&portsharing.DefaultPortSharer)
	api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer)
	api.PrebuildsReconciler.Store(&prebuilds.DefaultReconciler)
	buildInfo := codersdk.BuildInfoResponse{
		ExternalURL:           buildinfo.ExternalURL(),
		Version:               buildinfo.Version(),
		AgentAPIVersion:       AgentAPIVersionREST,
		ProvisionerAPIVersion: proto.CurrentVersion.String(),
		DashboardURL:          api.AccessURL.String(),
		WorkspaceProxy:        false,
		UpgradeMessage:        api.DeploymentValues.CLIUpgradeMessage.String(),
		DeploymentID:          api.DeploymentID,
		WebPushPublicKey:      api.WebpushDispatcher.PublicKey(),
		Telemetry:             api.Telemetry.Enabled(),
		OAuth2Provider:        api.DeploymentValues.OAuth2.Provider.Enable.Value(),
	}
	api.SiteHandler, err = site.New(&site.Options{
		CacheDir:                  siteCacheDir,
		Database:                  options.Database,
		Authorizer:                options.Authorizer,
		SiteFS:                    site.FS(),
		OAuth2Configs:             oauthConfigs,
		DocsURL:                   options.DeploymentValues.DocsURL.String(),
		AppearanceFetcher:         &api.AppearanceFetcher,
		BuildInfo:                 buildInfo,
		Entitlements:              options.Entitlements,
		Telemetry:                 options.Telemetry,
		Logger:                    options.Logger.Named("site"),
		AIGatewayEnabled:          options.DeploymentValues.AI.BridgeConfig.Enabled.Value(),
		UserSecretFilePathEnabled: !options.DeploymentValues.DisableUserSecretFilePath.Value(),
		ExperimentEvaluator:       api.ExperimentEvaluator,
	})
	if err != nil {
		options.Logger.Fatal(ctx, "failed to initialize site handler", slog.Error(err))
	}

	if options.UpdateCheckOptions != nil {
		api.updateChecker = updatecheck.New(
			options.Database,
			options.Logger.Named("update_checker"),
			*options.UpdateCheckOptions,
		)
	}

	if options.WorkspaceProxiesFetchUpdater == nil {
		options.WorkspaceProxiesFetchUpdater = &atomic.Pointer[healthcheck.WorkspaceProxiesFetchUpdater]{}
		var wpfu healthcheck.WorkspaceProxiesFetchUpdater = &healthcheck.AGPLWorkspaceProxiesFetchUpdater{}
		options.WorkspaceProxiesFetchUpdater.Store(&wpfu)
	}

	if options.HealthcheckFunc == nil {
		options.HealthcheckFunc = func(ctx context.Context, apiKey string, progress *healthcheck.Progress) *healthsdk.HealthcheckReport {
			// NOTE: dismissed healthchecks are marked in formatHealthcheck.
			// Not here, as this result gets cached.
			return healthcheck.Run(ctx, &healthcheck.ReportOptions{
				Database: healthcheck.DatabaseReportOptions{
					DB:        options.Database,
					Threshold: options.DeploymentValues.Healthcheck.ThresholdDatabase.Value(),
				},
				Websocket: healthcheck.WebsocketReportOptions{
					AccessURL: options.AccessURL,
					APIKey:    apiKey,
				},
				AccessURL: healthcheck.AccessURLReportOptions{
					AccessURL: options.AccessURL,
				},
				DerpHealth: derphealth.ReportOptions{
					DERPMap: api.DERPMap(),
				},
				WorkspaceProxy: healthcheck.WorkspaceProxyReportOptions{
					WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(),
				},
				ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportDeps{
					CurrentVersion:         buildinfo.Version(),
					CurrentAPIMajorVersion: proto.CurrentMajor,
					Store:                  options.Database,
					StaleInterval:          provisionerdserver.StaleInterval,
					// TimeNow set to default, see healthcheck/provisioner.go
				},
				Progress: progress,
			})
		}
	}

	if options.HealthcheckTimeout == 0 {
		options.HealthcheckTimeout = 30 * time.Second
	}
	if options.HealthcheckRefresh == 0 {
		options.HealthcheckRefresh = options.DeploymentValues.Healthcheck.Refresh.Value()
	}

	var oidcAuthURLParams map[string]string
	var oidcRedirectAllowedHosts []string
	var oidcRedirectDefaultScheme string
	if options.OIDCConfig != nil {
		oidcAuthURLParams = options.OIDCConfig.AuthURLParams
		oidcRedirectAllowedHosts = options.OIDCConfig.RedirectAllowedHosts
		oidcRedirectDefaultScheme = options.OIDCConfig.RedirectDefaultScheme
	}

	api.Auditor.Store(&options.Auditor)
	api.ConnectionLogger.Store(&options.ConnectionLogger)
	api.TailnetCoordinator.Store(&options.TailnetCoordinator)
	dialer := &InmemTailnetDialer{
		CoordPtr:            &api.TailnetCoordinator,
		DERPFn:              api.DERPMap,
		Logger:              options.Logger,
		ClientID:            uuid.New(),
		DatabaseHealthCheck: api.Database,
	}
	stn, err := NewServerTailnet(api.ctx,
		options.Logger,
		options.DERPServer,
		dialer,
		options.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
		options.DeploymentValues.DERP.Config.BlockDirect.Value(),
		api.TracerProvider,
	)
	if err != nil {
		panic("failed to setup server tailnet: " + err.Error())
	}
	api.agentProvider = stn

	{ // Chat daemon and git sync worker initialization.
		api.chatLimits = chatd.LimitsFromConfig(options.DeploymentValues.AI.Chat)
		maxChatsPerAcquire := options.DeploymentValues.AI.Chat.AcquireBatchSize.Value()
		if maxChatsPerAcquire > math.MaxInt32 {
			maxChatsPerAcquire = math.MaxInt32
		}
		if maxChatsPerAcquire < math.MinInt32 {
			maxChatsPerAcquire = math.MinInt32
		}
		streamSilenceTimeout := options.DeploymentValues.AI.Chat.StreamSilenceTimeout.Value()
		if streamSilenceTimeout == 0 {
			streamSilenceTimeout = chatloop.StreamSilenceTimeoutDisabled
		}

		var oidcMCPSrc mcpclient.UserOIDCTokenSource
		if options.OIDCConfig != nil {
			oidcMCPSrc = newOIDCMCPTokenSource(
				options.Database,
				options.OIDCConfig,
				options.Logger.Named("mcp-user-oidc"),
			)
		}

		// AI Gateway is mandatory for chat. When the bridge is disabled
		// the chat daemon stays nil and chat HTTP handlers return a
		// service-unavailable error with a clear remediation message.
		if options.DeploymentValues.AI.BridgeConfig.Enabled.Value() {
			var hookDispatcher *dispatch.Dispatcher
			chatConfig := options.DeploymentValues.AI.Chat
			hooksConfigured := chatConfig.HookURL.String() != "" && chatConfig.HookEnabled.Value()
			hooksExperimentEnabled := experiments.Enabled(codersdk.ExperimentAgentLifecycleHooks)
			if hooksConfigured && !hooksExperimentEnabled {
				options.Logger.Warn(ctx, "chat lifecycle hooks are configured but inactive; enable the agent-lifecycle-hooks experiment to activate them",
					slog.F("experiment", codersdk.ExperimentAgentLifecycleHooks),
				)
			}
			if hooksConfigured && hooksExperimentEnabled {
				if chatConfig.HookAllowInsecure.Value() && chatConfig.HookURL.Value().Scheme == "http" {
					options.Logger.Warn(ctx, "chat hooks use a plain HTTP URL; hook traffic is unencrypted and hook responses controlling agent execution can be forged on the network",
						slog.F("hook_url", mcpclient.RedactURL(chatConfig.HookURL.String())),
					)
				}
				hookDispatcher = dispatch.New(
					options.Logger,
					nil,
					chatConfig.HookURL.String(),
					chatConfig.HookAllowInsecure.Value(),
					chatConfig.HookSecret.Value(),
					chatConfig.HookTimeout.Value(),
					api.DeploymentID,
					buildinfo.Version(),
					options.PrometheusRegistry,
				)
			}
			api.chatDaemon, err = chatd.New(options.Pubsub, chatd.Config{
				Logger:                         options.Logger.Named("chatd"),
				Database:                       options.Database,
				ReplicaID:                      api.ID,
				StreamPartsDialer:              options.ChatStreamPartsDialer,
				MaxChatsPerAcquire:             int32(maxChatsPerAcquire), //nolint:gosec // maxChatsPerAcquire is clamped to int32 range above.
				AllowBYOK:                      options.DeploymentValues.AI.BridgeConfig.AllowBYOK.Value(),
				AllowBYOKSet:                   true,
				AIBridgeTransportFactory:       &api.AIBridgeTransportFactory,
				AlwaysEnableDebugLogs:          options.DeploymentValues.AI.Chat.DebugLoggingEnabled.Value(),
				StreamSilenceTimeout:           streamSilenceTimeout,
				DisableCallerSuppliedTools:     options.DeploymentValues.DisableChatCallerSuppliedTools.Value(),
				Experiments:                    experiments,
				AgentConn:                      api.agentProvider.AgentConn,
				AgentInactiveDisconnectTimeout: api.AgentInactiveDisconnectTimeout,
				CreateWorkspace:                api.chatCreateWorkspace,
				StartWorkspace:                 api.chatStartWorkspace,
				StopWorkspace:                  api.chatStopWorkspace,
				RenderTemplateParameters:       api.chatRenderTemplateParameters,
				WebpushDispatcher:              options.WebPushDispatcher,
				HookDispatcher:                 hookDispatcher,
				UsageTracker:                   options.WorkspaceUsageTracker,
				PrometheusRegistry:             options.PrometheusRegistry,
				TracerProvider:                 options.TracerProvider,
				AgentCapacityUnlock:            options.ChatAgentCapacityUnlock,
				OIDCTokenSource:                oidcMCPSrc,
				MCPHTTPClient:                  api.mcpHTTPClient,
				InternalMCPServers:             options.ChatInternalMCPServers,
				NotificationsEnqueuer:          options.NotificationsEnqueuer,
				Auditor:                        &api.Auditor,
				Limits:                         api.chatLimits,
			})
			if err != nil {
				panic(xerrors.Errorf("create chat daemon: %w", err))
			}
			if !options.ChatWorkerDisabled {
				api.chatDaemon.Start()
			}
		}
		gitSyncLogger := options.Logger.Named("gitsync")
		refresher := gitsync.NewRefresher(
			api.resolveGitProvider,
			api.resolveChatGitAccessToken,
			gitSyncLogger.Named("refresher"),
			quartz.NewReal(),
		)
		publishDiffStatusChange := chatDaemonPublishDiffStatusChangeFunc(api.chatDaemon)
		api.gitSyncWorker = gitsync.NewWorker(options.Database,
			refresher,
			publishDiffStatusChange,
			quartz.NewReal(),
			gitSyncLogger,
		)
		// nolint:gocritic // chat diff worker needs to be able to CRUD chats.
		go api.gitSyncWorker.Start(dbauthz.AsChatd(api.ctx))
	}
	if options.DeploymentValues.Prometheus.Enable {
		options.PrometheusRegistry.MustRegister(stn)
		api.lifecycleMetrics = agentapi.NewLifecycleMetrics(options.PrometheusRegistry)
		api.workspaceAgentRPCMetrics = NewWorkspaceAgentRPCMetrics(options.PrometheusRegistry, options.Logger)
	}
	api.NetworkTelemetryBatcher = tailnet.NewNetworkTelemetryBatcher(
		quartz.NewReal(),
		api.NetworkTelemetryBatchFrequency,
		api.NetworkTelemetryBatchMaxSize,
		api.handleNetworkTelemetry,
	)
	if options.CoordinatorResumeTokenProvider == nil {
		panic("CoordinatorResumeTokenProvider is nil")
	}
	api.TailnetClientService, err = tailnet.NewClientService(tailnet.ClientServiceOptions{
		Logger:                   api.Logger.Named("tailnetclient"),
		CoordPtr:                 &api.TailnetCoordinator,
		DERPMapUpdateFrequency:   api.DERPMapUpdateFrequency,
		DERPMapFn:                api.DERPMap,
		NetworkTelemetryHandler:  api.NetworkTelemetryBatcher.Handler,
		ResumeTokenProvider:      api.CoordinatorResumeTokenProvider,
		WorkspaceUpdatesProvider: api.UpdatesProvider,
	})
	if err != nil {
		api.Logger.Fatal(context.Background(), "failed to initialize tailnet client service", slog.Error(err))
	}

	api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{
		Database:               options.Database,
		Logger:                 options.Logger.Named("workspacestats"),
		Pubsub:                 options.Pubsub,
		TemplateScheduleStore:  options.TemplateScheduleStore,
		StatsBatcher:           options.StatsBatcher,
		UsageTracker:           options.WorkspaceUsageTracker,
		UpdateAgentMetricsFn:   options.UpdateAgentMetrics,
		AppStatBatchSize:       workspaceapps.DefaultStatsDBReporterBatchSize,
		DisableDatabaseInserts: !options.DeploymentValues.StatsCollection.UsageStats.Enable.Value(),
	})

	// Initialize the metadata batcher for batching agent metadata updates.
	batcherOpts := []metadatabatcher.Option{
		metadatabatcher.WithLogger(options.Logger.Named("metadata_batcher")),
	}
	batcherOpts = append(batcherOpts, options.MetadataBatcherOptions...)
	api.metadataBatcher, err = metadatabatcher.NewBatcher(
		api.ctx,
		options.PrometheusRegistry,
		options.Database,
		options.Pubsub,
		batcherOpts...,
	)
	if err != nil {
		api.Logger.Fatal(context.Background(), "failed to initialize metadata batcher", slog.Error(err))
	}

	workspaceAppsLogger := options.Logger.Named("workspaceapps")
	if options.WorkspaceAppsStatsCollectorOptions.Logger == nil {
		named := workspaceAppsLogger.Named("stats_collector")
		options.WorkspaceAppsStatsCollectorOptions.Logger = &named
	}
	if options.WorkspaceAppsStatsCollectorOptions.Reporter == nil {
		options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter
	}

	wsMetrics := httpmw.NewWSMetrics(options.PrometheusRegistry)
	api.chatWorkspaceUploadsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "coderd",
		Subsystem: "chat",
		Name:      "workspace_upload_total",
		Help:      "Total chat workspace file uploads by HTTP response status.",
	}, []string{"status"})
	options.PrometheusRegistry.MustRegister(api.chatWorkspaceUploadsTotal)
	api.wsWatcher = httpapi.NewWSWatcher(options.Clock, wsMetrics.RecordProbe)

	api.workspaceAppServer = workspaceapps.NewServer(workspaceapps.ServerOptions{
		Logger: workspaceAppsLogger,

		DashboardURL:  api.AccessURL,
		AccessURL:     api.AccessURL,
		Hostname:      api.AppHostname,
		HostnameRegex: api.AppHostnameRegex,
		RealIPConfig:  options.RealIPConfig,

		SignedTokenProvider: api.WorkspaceAppsProvider,
		AgentProvider:       api.agentProvider,
		StatsCollector:      workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions),
		WSWatcher:           api.wsWatcher,

		DisablePathApps:          options.DeploymentValues.DisablePathApps.Value(),
		CookiesConfig:            options.DeploymentValues.HTTPCookies,
		APIKeyEncryptionKeycache: options.AppEncryptionKeyCache,
	})

	api.workspaceAgentConnWatcher = workspaceconnwatcher.New(api.ctx, options.Logger, options.Pubsub, options.Database)

	api.workspaceBuildOrchestrator = wsbuildorchestrator.New(wsbuildorchestrator.Options{
		Logger:            options.Logger,
		Database:          options.Database,
		Pubsub:            options.Pubsub,
		FileCache:         api.FileCache,
		BuildUsageChecker: api.BuildUsageChecker,
		DeploymentValues:  options.DeploymentValues,
		Experiments:       api.Experiments,
		BuilderMetrics:    options.WorkspaceBuilderMetrics,
		Clock:             quartz.NewReal(),
	})
	api.workspaceBuildOrchestrator.Start(api.ctx)

	// The OAuth2 provider is opt-in. The flag is read once at startup, here
	// and in the build info response and the AI bridge config, so a runtime
	// toggle would have to update all three.
	oauth2ProviderEnabled := api.DeploymentValues.OAuth2.Provider.Enable.Value()
	apiKeyConfig := httpmw.ExtractAPIKeyConfig{
		DB:                            options.Database,
		ActivateDormantUser:           ActivateDormantUser(options.Logger, &api.Auditor, options.Database),
		OAuth2Configs:                 oauthConfigs,
		RedirectToLogin:               false,
		DisableSessionExpiryRefresh:   options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
		Optional:                      false,
		SessionTokenFunc:              nil, // Default behavior
		PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
		Logger:                        options.Logger,
		AccessURL:                     options.AccessURL,
	}
	apiKeyMiddleware := httpmw.ExtractAPIKeyMW(apiKeyConfig)
	// Same as above but it redirects to the login page.
	apiKeyMiddlewareRedirect := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
		DB:                            options.Database,
		OAuth2Configs:                 oauthConfigs,
		RedirectToLogin:               true,
		DisableSessionExpiryRefresh:   options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
		Optional:                      false,
		SessionTokenFunc:              nil, // Default behavior
		PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
		Logger:                        options.Logger,
		AccessURL:                     options.AccessURL,
	})
	// Same as the first but it's optional.
	apiKeyMiddlewareOptional := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
		DB:                            options.Database,
		OAuth2Configs:                 oauthConfigs,
		RedirectToLogin:               false,
		DisableSessionExpiryRefresh:   options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
		Optional:                      true,
		SessionTokenFunc:              nil, // Default behavior
		PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
		Logger:                        options.Logger,
		AccessURL:                     options.AccessURL,
	})

	workspaceAgentInfo := httpmw.ExtractWorkspaceAgentAndLatestBuild(httpmw.ExtractWorkspaceAgentAndLatestBuildConfig{
		DB:       options.Database,
		Optional: false,
	})

	// API rate limit middleware. The counter is local and not shared between
	// replicas or instances of this middleware.
	apiRateLimiter := httpmw.RateLimit(options.APIRateLimit, time.Minute)

	// Register DERP on expvar HTTP handler, which we serve below in the router, c.f. expvar.Handler()
	expDERPOnce.Do(func() {
		// We need to do this via a global Once because expvar registry is global and panics if we
		// register multiple times.  In production there is only one Coderd and one DERP server per
		// process, but in testing, we create multiple of both, so the Once protects us from
		// panicking.
		if options.DERPServer != nil && expvar.Get("derp") == nil {
			expvar.Publish("derp", api.DERPServer.ExpVar())
		}
	})
	if options.PrometheusRegistry != nil && options.DERPServer != nil {
		options.PrometheusRegistry.MustRegister(derpmetrics.NewDERPExpvarCollector(options.DERPServer))
	}
	cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
	prometheusMW := httpmw.Prometheus(options.PrometheusRegistry, wsMetrics)

	r.Use(
		sharedhttpmw.Recover(api.Logger),
		httpmw.WithProfilingLabels,
		tracing.StatusWriterMiddleware,
		options.DeploymentValues.HTTPCookies.Middleware,
		tracing.Middleware(api.TracerProvider, tracing.DefaultRoutePatterns, "coderd"),
		httpmw.AttachRequestID,
		httpmw.ExtractRealIP(api.RealIPConfig),
		loggermw.Logger(api.Logger, func(r *http.Request) string {
			return httpmw.EffectiveHost(api.RealIPConfig, r)
		}),
		singleSlashMW,
		rolestore.CustomRoleMW,
		// Validate API key on every request (if present) and store
		// the result in context. The rate limiter reads this to key
		// by user ID, and downstream ExtractAPIKeyMW reuses it to
		// avoid redundant DB lookups. Never rejects requests.
		httpmw.PrecheckAPIKey(httpmw.ValidateAPIKeyConfig{
			DB:                          options.Database,
			OAuth2Configs:               oauthConfigs,
			DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
			Logger:                      options.Logger,
		}),
		httpmw.HTTPRoute, // NB: prometheusMW depends on this middleware.
		prometheusMW,
		// Build-Version is helpful for debugging.
		func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Header().Add(codersdk.BuildVersionHeader, buildinfo.Version())
				next.ServeHTTP(w, r)
			})
		},
		// SubdomainAppMW checks if the first subdomain is a valid app URL. If
		// it is, it will serve that application.
		//
		// Workspace apps do their own auth and CORS and must be BEFORE the auth
		// and CORS middleware.
		api.workspaceAppServer.HandleSubdomain(apiRateLimiter),
		cors,
		// This header stops a browser from trying to MIME-sniff the content type and
		// forces it to stick with the declared content-type. This is the only valid
		// value for this header.
		// See: https://github.com/coder/security/issues/12
		func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Header().Add("X-Content-Type-Options", "nosniff")
				next.ServeHTTP(w, r)
			})
		},
		httpmw.CSRF(options.DeploymentValues.HTTPCookies),
	)

	// This incurs a performance hit from the middleware, but is required to make sure
	// we do not override subdomain app routes.
	r.Get("/latency-check", tracing.StatusWriterMiddleware(prometheusMW(LatencyCheck())).ServeHTTP)

	r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("OK")) })

	// Attach workspace apps routes.
	r.Group(func(r chi.Router) {
		r.Use(apiRateLimiter)
		api.workspaceAppServer.Attach(r)
	})

	if options.DERPServer != nil {
		derpHandler := derphttp.Handler(api.DERPServer)
		derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler)

		r.Route("/derp", func(r chi.Router) {
			r.Get("/", derpHandler.ServeHTTP)
			// This is used when UDP is blocked, and latency must be checked via HTTP(s).
			r.Get("/latency-check", func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(http.StatusOK)
			})
		})
	}

	// Register callback handlers for each OAuth2 provider.
	// We must support gitauth and externalauth for backwards compatibility.
	for _, route := range []string{"gitauth", "external-auth"} {
		r.Route("/"+route, func(r chi.Router) {
			for _, externalAuthConfig := range options.ExternalAuthConfigs {
				// We don't need to register a callback handler for device auth.
				if externalAuthConfig.DeviceAuth != nil {
					continue
				}
				r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) {
					r.Use(
						apiKeyMiddlewareRedirect,
						httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil, externalAuthConfig.CodeChallengeMethodsSupported, nil, ""),
					)
					r.Get("/", api.externalAuthCallback(externalAuthConfig))
				})
			}
		})
	}

	// OAuth2 metadata endpoint for RFC 8414 discovery
	r.Route("/.well-known/oauth-authorization-server", func(r chi.Router) {
		r.Use(
			httpmw.RequireOAuth2Provider(oauth2ProviderEnabled),
			// Discovery carries no credential, so the limiter keys on the
			// address rather than a user.
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
		)
		r.Get("/*", api.oauth2AuthorizationServerMetadata())
	})
	// OAuth2 protected resource metadata endpoint for RFC 9728 discovery
	r.Route("/.well-known/oauth-protected-resource", func(r chi.Router) {
		r.Use(
			httpmw.RequireOAuth2Provider(oauth2ProviderEnabled),
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
		)
		r.Get("/*", api.oauth2ProtectedResourceMetadata())
	})

	// OAuth2 linking routes do not make sense under the /api/v2 path.  These are
	// for an external application to use Coder as an OAuth2 provider, not for
	// logging into Coder with an external OAuth2 provider.
	r.Route("/oauth2", func(r chi.Router) {
		r.Use(
			httpmw.RequireOAuth2Provider(oauth2ProviderEnabled),
			// Every response from this tree may carry a credential, so none of
			// them may be retained by an intermediary cache. Mounted after
			// the gate, so a request the gate rejects gets no headers. That
			// rejection carries no credential, so it needs none.
			httpmw.NoStore,
		)
		authorize := r.With(
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
			// Fetch the app as system for the authorize endpoint
			httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderAppWithOAuth2Errors(options.Database)),
			apiKeyMiddlewareRedirect,
		)
		// GET shows the consent page, POST processes the consent
		authorize.Get("/authorize", api.getOAuth2ProviderAppAuthorize())
		authorize.Post("/authorize", api.postOAuth2ProviderAppAuthorize())

		tokens := r.With(
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
			// Use OAuth2-compliant error responses for the tokens endpoint
			httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderAppWithOAuth2Errors(options.Database)),
		)
		// DELETE on /tokens is not part of the OAuth2 spec.  It is our own
		// route used to revoke permissions from an application.  It is here for
		// parity with POST on /tokens.
		tokens.With(apiKeyMiddleware).Delete("/tokens", api.deleteOAuth2ProviderAppTokens())
		// The POST /tokens endpoint will be called from an unauthorized client so
		// we cannot require an API key.
		tokens.Post("/tokens", api.postOAuth2ProviderAppToken())

		// RFC 7009 Token Revocation Endpoint
		r.With(
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
			// RFC 7009 endpoint uses OAuth2 client authentication, not API key
			httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderAppWithOAuth2Errors(options.Database)),
		).Post("/revoke", api.revokeOAuth2Token())

		// RFC 7591 Dynamic Client Registration - Public endpoint
		r.With(httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute)).
			Post("/register", api.postOAuth2ClientRegistration())

		// RFC 7592 Client Configuration Management - Protected by registration access token
		clients := r.With(
			httpmw.RateLimitOAuth2(options.LoginRateLimit, time.Minute),
			// Middleware to validate registration access token
			oauth2provider.RequireRegistrationAccessToken(api.Database),
		)
		clients.Get("/clients/{client_id}", api.oauth2ClientConfiguration())          // Read client configuration
		clients.Put("/clients/{client_id}", api.putOAuth2ClientConfiguration())       // Update client configuration
		clients.Delete("/clients/{client_id}", api.deleteOAuth2ClientConfiguration()) // Delete client
	})

	// Experimental routes are not guaranteed to be stable and may change at any time.
	r.Route("/api/experimental", func(r chi.Router) {
		api.ExperimentalHandler = r

		r.NotFound(func(rw http.ResponseWriter, _ *http.Request) { httpapi.RouteNotFound(rw) })
		r.Use(
			apiRateLimiter,
			httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
		)

		r.Route("/users/email", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Put("/", api.putUserEmailExperimental)
		})

		// NOTE(DanielleMaywood):
		r.Route("/users/{user}/skills", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.ExtractUserParam(options.Database),
			)
			r.Post("/", api.postUserSkill)
			r.Get("/", api.getUserSkills)
			r.Route("/{skillName}", func(r chi.Router) {
				r.Get("/", api.getUserSkill)
				r.Patch("/", api.patchUserSkill)
				r.Delete("/", api.deleteUserSkill)
			})
		})
		r.Route("/organizations/{organization}/skills", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.ExtractOrganizationParam(options.Database),
			)
			r.Get("/", api.getOrganizationSkills)
			r.Post("/", api.postOrganizationSkill)
			r.Route("/{skillName}", func(r chi.Router) {
				r.With(httpmw.ExtractOrganizationSkillParam(options.Database, api.HTTPAuth.Authorize,
					policy.ActionRead)).Get("/", api.getOrganizationSkill)
				// PATCH returns the skill's content, so it requires read like GET.
				// Readers reach the handlers, where the database wrapper denies
				// the mutation with 403 rather than concealing the skill.
				r.With(httpmw.ExtractOrganizationSkillParam(options.Database, api.HTTPAuth.Authorize,
					policy.ActionRead)).Patch("/", api.patchOrganizationSkill)
				r.With(httpmw.ExtractOrganizationSkillParam(options.Database, api.HTTPAuth.Authorize,
					policy.ActionRead, policy.ActionDelete)).Delete("/", api.deleteOrganizationSkill)
				r.Route("/acl", func(r chi.Router) {
					r.Use(httpmw.ExtractOrganizationSkillParam(options.Database, api.HTTPAuth.Authorize, policy.ActionShare))
					r.Get("/", api.getOrganizationSkillACL)
					r.Patch("/", api.patchOrganizationSkillACL)
					r.Get("/available", api.getOrganizationSkillACLAvailable)
				})
			})
		})
		api.registerExperimentalChatRoutes(r, apiKeyMiddleware)
		r.Route("/organizations/{organization}", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.ExtractOrganizationParam(options.Database),
			)
			api.registerExperimentalOrganizationChatRoutes(r)
			// These stay outside /skills, where {skillName} would capture them.
			r.Route("/skill-settings", func(r chi.Router) {
				r.Get("/", api.getOrganizationSkillSettings)
				r.Put("/", api.putOrganizationSkillSettings)
			})
			r.Route("/members/{user}/skill-preferences", func(r chi.Router) {
				r.Use(httpmw.ExtractOrganizationMemberParam(options.Database))
				r.Get("/", api.getOrganizationSkillPreferences)
				r.Patch("/", api.patchOrganizationSkillPreferences)
			})
		})

		r.Route("/mcp", func(r chi.Router) {
			// Providers pin the redirect URI when a session is established,
			// so the callback URL cannot change for existing sessions without
			// breaking token refresh and forcing a re-auth.
			r.With(apiKeyMiddleware).Get("/servers/{mcpServer}/oauth2/callback", api.mcpServerOAuth2Callback)
			// MCP HTTP transport endpoint with mandatory authentication.
			r.Route("/http", func(r chi.Router) {
				r.Use(func(next http.Handler) http.Handler {
					cfg := apiKeyConfig
					cfg.ResourceURI = options.AccessURL.JoinPath(mcp.MCPEndpoint).String()
					cfg.ResourceMetadataURL = options.AccessURL.JoinPath("/.well-known/oauth-protected-resource", mcp.MCPEndpoint).String()
					canonical := httpmw.ExtractAPIKeyMW(cfg)(next)
					cfg.ResourceMetadataURL = options.AccessURL.JoinPath("/.well-known/oauth-protected-resource", mcp.MCPEndpoint+"/").String()
					trailingSlash := httpmw.ExtractAPIKeyMW(cfg)(next)
					return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
						// Routing normalizes slashes, but OAuth resource identity
						// must not admit aliases or arbitrary descendant paths.
						switch r.URL.EscapedPath() {
						case mcp.MCPEndpoint:
							canonical.ServeHTTP(rw, r)
						case mcp.MCPEndpoint + "/":
							trailingSlash.ServeHTTP(rw, r)
						default:
							httpapi.RouteNotFound(rw)
						}
					})
				})
				r.Use(
					httpmw.RequireOAuth2Provider(oauth2ProviderEnabled),
					httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentMCPServerHTTP),
				)
				r.Mount("/", api.mcpHTTPHandler())
			})
		})
		r.Route("/experiments/rules", func(r chi.Router) {
			// Responses contain condition text, so they are never cached.
			r.Use(apiKeyMiddleware, httpmw.NoStore)
			r.Get("/", api.experimentRules)
			r.Put("/{experiment}", api.putExperimentRule)
		})
		r.Route("/watch-all-workspacebuilds", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentWorkspaceBuildUpdates),
			)
			r.Get("/", api.watchAllWorkspaceBuilds)
		})
	})

	r.Route("/api/v2", func(r chi.Router) {
		api.APIHandler = r

		r.NotFound(func(rw http.ResponseWriter, _ *http.Request) { httpapi.RouteNotFound(rw) })
		r.Use(
			apiRateLimiter,
			httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
		)
		r.Get("/", apiRoot)
		// All CSP errors will be logged
		r.Post("/csp/reports", api.logReportCSPViolations)

		r.Get("/auth/scopes", api.listExternalScopes)

		r.Get("/buildinfo", buildInfoHandler(buildInfo))
		// /regions is overridden in the enterprise version
		r.Group(func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Get("/regions", api.regions)
		})
		r.Route("/derp-map", func(r chi.Router) {
			r.Get("/", api.derpMapUpdates)
		})
		r.Route("/deployment", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Get("/config", api.deploymentValues)
			r.Get("/stats", api.deploymentStats)
			r.Get("/ssh", api.sshConfig)
			r.Get("/user-secrets/capabilities", api.userSecretsCapabilities)
			r.Post("/premium-funnel-events", api.postPremiumFunnelEvent)
		})
		r.Route("/experiments", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Get("/available", handleExperimentsAvailable)
			r.With(httpmw.NoStore).Get("/", api.handleExperimentsGet)
		})
		api.registerChatAPIRoutes(r, apiKeyMiddleware)
		r.Route("/mcp", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			// Disconnect stays outside organization routes so former organization
			// members can delete their stored token after losing config read access.
			r.Delete("/servers/{mcpServer}/oauth2/disconnect", api.mcpServerOAuth2Disconnect)
		})

		r.Get("/updatecheck", api.updateCheck)
		r.Route("/audit", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				// This middleware only checks the site and orgs for the audit_log read
				// permission.
				// In the future if it makes sense to have this permission on the user as
				// well we will need to update this middleware to include that check.
				func(next http.Handler) http.Handler {
					return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
						if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog) {
							next.ServeHTTP(rw, r)
							return
						}

						if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog.AnyOrganization()) {
							next.ServeHTTP(rw, r)
							return
						}

						httpapi.Forbidden(rw)
					})
				},
			)

			r.Get("/", api.auditLogs)
			r.Post("/testgenerate", api.generateFakeAuditLog)
		})

		r.Route("/files", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.RateLimit(options.FilesRateLimit, time.Minute),
			)
			r.Get("/{fileID}", api.fileByID)
			r.Post("/", api.postFile)
		})
		r.Route("/external-auth", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
			)
			// Get without a specific external auth ID will return all external auths.
			r.Get("/", api.listUserExternalAuths)
			r.Route("/{externalauth}", func(r chi.Router) {
				r.Use(
					httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs),
				)
				r.Delete("/", api.deleteExternalAuthByID)
				r.Get("/", api.externalAuthByID)
				r.Post("/device", api.postExternalAuthDeviceByID)
				r.Get("/device", api.externalAuthDeviceByID)
			})
		})
		r.Route("/organizations", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
			)
			r.Get("/", api.organizations)
			r.Route("/{organization}", func(r chi.Router) {
				r.Use(
					httpmw.ExtractOrganizationParam(options.Database),
				)
				api.registerOrganizationChatRoutes(r)
				r.Get("/", api.organization)
				r.Post("/templateversions", api.postTemplateVersionsByOrganization)
				r.Route("/templates", func(r chi.Router) {
					r.Post("/", api.postTemplateByOrganization)
					r.Get("/", api.templatesByOrganization())
					r.Get("/examples", api.templateExamplesByOrganization)
					r.Route("/{templatename}", func(r chi.Router) {
						r.Get("/", api.templateByOrganizationAndName)
						r.Route("/versions/{templateversionname}", func(r chi.Router) {
							r.Get("/", api.templateVersionByOrganizationTemplateAndName)
							r.Get("/previous", api.previousTemplateVersionByOrganizationTemplateAndName)
						})
					})
				})
				r.Get("/paginated-members", api.paginatedMembers)
				r.Route("/members", func(r chi.Router) {
					r.Get("/", api.listMembers)
					r.Route("/roles", func(r chi.Router) {
						r.Get("/", api.assignableOrgRoles)
					})

					r.Route("/{user}", func(r chi.Router) {
						r.Group(func(r chi.Router) {
							r.Use(
								// Adding a member requires "read" permission
								// on the site user. So limited to owners and user-admins.
								// TODO: Allow org-admins to add users via some new permission? Or give them
								// 	read on site users.
								httpmw.ExtractUserParam(options.Database),
							)
							r.Post("/", api.postOrganizationMember)
						})

						r.Group(func(r chi.Router) {
							r.Use(
								httpmw.ExtractOrganizationMemberParam(options.Database),
							)
							api.registerOrganizationMemberChatRoutes(r)
							r.Get("/", api.organizationMember)
							r.Delete("/", api.deleteOrganizationMember)
							r.Put("/roles", api.putMemberRoles)
							r.Route("/workspaces", func(r chi.Router) {
								r.Post("/", api.postWorkspacesByOrganization)
								r.Get("/available-users", api.workspaceAvailableUsers)
							})
						})
					})
				})
				r.Route("/provisionerdaemons", func(r chi.Router) {
					r.Get("/", api.provisionerDaemons)
				})
				r.Route("/provisionerjobs", func(r chi.Router) {
					r.Get("/{job}", api.provisionerJob)
					r.Get("/", api.provisionerJobs)
				})
			})
		})
		r.Route("/templates", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
			)
			r.Get("/", api.fetchTemplates(nil))
			r.Get("/examples", api.templateExamples)
			r.Route("/{template}", func(r chi.Router) {
				r.Use(
					httpmw.ExtractTemplateParam(options.Database),
				)
				r.Get("/daus", api.templateDAUs)
				r.Get("/", api.template)
				r.Delete("/", api.deleteTemplate)
				r.Patch("/", api.patchTemplateMeta)
				r.Route("/versions", func(r chi.Router) {
					r.Post("/archive", api.postArchiveTemplateVersions)
					r.Get("/", api.templateVersionsByTemplate)
					r.Patch("/", api.patchActiveTemplateVersion)
					r.Get("/{templateversionname}", api.templateVersionByName)
				})
			})
		})

		r.Route("/templateversions/{templateversion}", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.ExtractTemplateVersionParam(options.Database),
			)
			r.Get("/", api.templateVersion)
			r.Patch("/", api.patchTemplateVersion)
			r.Patch("/cancel", api.patchCancelTemplateVersion)
			r.Post("/archive", api.postArchiveTemplateVersion())
			r.Post("/unarchive", api.postUnarchiveTemplateVersion())
			// Old agents may expect a non-error response from /schema and /parameters endpoints.
			// The idea is to return an empty [], so that the coder CLI won't get blocked accidentally.
			r.Get("/schema", templateVersionSchemaDeprecated)
			r.Get("/parameters", templateVersionParametersDeprecated)
			r.Get("/rich-parameters", api.templateVersionRichParameters)
			r.Get("/external-auth", api.templateVersionExternalAuth)
			r.Get("/variables", api.templateVersionVariables)
			r.Get("/presets", api.templateVersionPresets)
			r.Get("/resources", api.templateVersionResources)
			r.Get("/logs", api.templateVersionLogs)
			r.Route("/dry-run", func(r chi.Router) {
				r.Post("/", api.postTemplateVersionDryRun)
				r.Get("/{jobID}", api.templateVersionDryRun)
				r.Get("/{jobID}/resources", api.templateVersionDryRunResources)
				r.Get("/{jobID}/logs", api.templateVersionDryRunLogs)
				r.Get("/{jobID}/matched-provisioners", api.templateVersionDryRunMatchedProvisioners)
				r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
			})

			r.Group(func(r chi.Router) {
				r.Route("/dynamic-parameters", func(r chi.Router) {
					r.Post("/evaluate", api.templateVersionDynamicParametersEvaluate)
					r.Get("/", api.templateVersionDynamicParametersWebsocket)
				})
			})
		})
		if !api.DeploymentValues.TemplateBuilder.Disabled.Value() {
			r.Route("/templatebuilder", func(r chi.Router) {
				r.Use(
					apiKeyMiddleware,
				)
				r.Get("/bases", api.templateBuilderBases)
				r.Get("/modules", api.templateBuilderModules)
				r.Post("/compose", api.templateBuilderCompose)
				r.Post("/compose/template", api.templateBuilderCreateTemplate)
				r.Post("/sessions", api.templateBuilderSession)
			})
		}

		r.Route("/users", func(r chi.Router) {
			r.Get("/first", api.firstUser)
			r.Post("/first", api.postFirstUser)
			r.Get("/authmethods", api.userAuthMethods)

			r.Group(func(r chi.Router) {
				// We use a tight limit for password login to protect against
				// audit-log write DoS, pbkdf2 DoS, and simple brute-force
				// attacks.
				//
				// This value is intentionally increased during tests.
				r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute))
				r.Post("/login", api.postLogin)
				r.Post("/otp/request", api.postRequestOneTimePasscode)
				r.Post("/validate-password", api.validateUserPassword)
				r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode)
				r.Route("/oauth2", func(r chi.Router) {
					r.Get("/github/device", api.userOAuth2GithubDevice)
					r.Route("/github", func(r chi.Router) {
						r.Use(
							// Github supports PKCE S256
							httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil, options.GithubOAuth2Config.PKCESupported(), nil, ""),
						)
						r.Get("/callback", api.userOAuth2Github)
					})
				})
				r.Route("/oidc/callback", func(r chi.Router) {
					r.Use(
						httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams, options.OIDCConfig.PKCESupported(), oidcRedirectAllowedHosts, oidcRedirectDefaultScheme),
					)
					r.Get("/", api.userOIDC)
				})
			})
			r.Group(func(r chi.Router) {
				r.Use(
					apiKeyMiddleware,
				)
				r.Post("/", api.postUser)
				r.Get("/", api.users)
				r.Post("/logout", api.postLogout)
				r.Get("/oidc-claims", api.userOIDCClaims)
				// These routes query information about site wide roles.
				r.Route("/roles", func(r chi.Router) {
					r.Get("/", api.AssignableSiteRoles)
				})
				r.Route("/{user}", func(r chi.Router) {
					r.Group(func(r chi.Router) {
						r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize))
						// Creating workspaces does not require permissions on the user, only the
						// organization member. This endpoint should match the authz story of
						// postWorkspacesByOrganization
						r.Post("/workspaces", api.postUserWorkspaces)
						r.Route("/workspace/{workspacename}", func(r chi.Router) {
							r.Get("/", api.workspaceByOwnerAndName)
							r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber)
						})
					})

					r.Group(func(r chi.Router) {
						r.Use(httpmw.ExtractUserParam(options.Database))

						r.Route("/ai-provider-keys", api.registerUserAIProviderKeyRoutes)
						r.Post("/convert-login", api.postConvertLoginType)
						r.Delete("/", api.deleteUser)
						r.Get("/", api.userByName)
						r.Get("/login-type", api.userLoginType)
						r.Put("/profile", api.putUserProfile)
						r.Route("/status", func(r chi.Router) {
							r.Put("/suspend", api.putSuspendUserAccount())
							r.Put("/activate", api.putActivateUserAccount())
						})
						r.Get("/appearance", api.userAppearanceSettings)
						r.Put("/appearance", api.putUserAppearanceSettings)
						r.Get("/preferences", api.userPreferenceSettings)
						r.Put("/preferences", api.putUserPreferenceSettings)

						r.Route("/password", func(r chi.Router) {
							r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute))
							r.Put("/", api.putUserPassword)
						})
						// These roles apply to the site wide permissions.
						r.Put("/roles", api.putUserRoles)
						r.Get("/roles", api.userRoles)

						r.Route("/keys", func(r chi.Router) {
							r.Post("/", api.postAPIKey)
							r.Route("/tokens", func(r chi.Router) {
								r.Post("/", api.postToken)
								r.Get("/", api.tokens)
								r.Get("/tokenconfig", api.tokenConfig)
								r.Route("/{keyname}", func(r chi.Router) {
									r.Get("/", api.apiKeyByName)
								})
							})
							r.Route("/{keyid}", func(r chi.Router) {
								r.Get("/", api.apiKeyByID)
								r.Delete("/", api.deleteAPIKey)
								r.Put("/expire", api.expireAPIKey)
							})
						})

						r.Route("/organizations", func(r chi.Router) {
							r.Get("/", api.organizationsByUser)
							r.Get("/{organizationname}", api.organizationByUserAndName)
						})

						r.Get("/gitsshkey", api.gitSSHKey)
						r.Put("/gitsshkey", api.regenerateGitSSHKey)
						r.Route("/secrets", func(r chi.Router) {
							r.Post("/", api.postUserSecret)
							r.Post("/batch", api.postUserSecretsBatch)
							r.Get("/", api.getUserSecrets)
							r.Route("/{name}", func(r chi.Router) {
								r.Get("/", api.getUserSecret)
								r.Patch("/", api.patchUserSecret)
								r.Delete("/", api.deleteUserSecret)
							})
						})
						r.Route("/notifications", func(r chi.Router) {
							r.Route("/preferences", func(r chi.Router) {
								r.Get("/", api.userNotificationPreferences)
								r.Put("/", api.putUserNotificationPreferences)
							})
						})
						r.Route("/webpush", func(r chi.Router) {
							r.Post("/subscription", api.postUserWebpushSubscription)
							r.Delete("/subscription", api.deleteUserWebpushSubscription)
							r.Post("/test", api.postUserPushNotificationTest)
						})
					})
				})
			})
		})
		r.Route("/workspaceagents", func(r chi.Router) {
			r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity)
			r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity)
			r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
			r.With(
				apiKeyMiddlewareOptional,
				httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
					DB:       options.Database,
					Optional: true,
				}),
				httpmw.RequireAPIKeyOrWorkspaceProxyAuth(),
			).Get("/connection", api.workspaceAgentConnectionGeneric)
			r.Route("/me", func(r chi.Router) {
				r.Use(workspaceAgentInfo)
				r.Group(func(r chi.Router) {
					r.Use(
						// Override the request_type for agent rpc traffic.
						httpmw.WithStaticProfilingLabels(pprof.Labels(pproflabel.RequestTypeTag, "agent-rpc")),
					)
					r.Get("/rpc", api.workspaceAgentRPC)
				})
				r.Patch("/logs", api.patchWorkspaceAgentLogs)
				r.Patch("/app-status", api.patchWorkspaceAgentAppStatus)
				// Deprecated: Required to support legacy agents
				r.Get("/gitauth", api.workspaceAgentsGitAuth)
				r.Get("/external-auth", api.workspaceAgentsExternalAuth)
				r.Get("/gitsshkey", api.agentGitSSHKey)
				r.Post("/log-source", api.workspaceAgentPostLogSource)
				r.Get("/reinit", api.workspaceAgentReinit)
				r.Route("/experimental", func(r chi.Router) {
					r.Post("/chat-context/refresh", api.workspaceAgentRefreshChatContext)
				})
			})
			r.Route("/{workspaceagent}", func(r chi.Router) {
				r.Use(
					// Allow either API key or external workspace proxy auth and require it.
					apiKeyMiddlewareOptional,
					httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
						DB:       options.Database,
						Optional: true,
					}),
					httpmw.RequireAPIKeyOrWorkspaceProxyAuth(),
					httpmw.ExtractWorkspaceAgentAndWorkspaceParam(options.Database),
				)
				r.Get("/", api.workspaceAgent)
				r.Get("/watch-metadata", api.watchWorkspaceAgentMetadataSSE)
				r.Get("/watch-metadata-ws", api.watchWorkspaceAgentMetadataWS)
				r.Get("/startup-logs", api.workspaceAgentLogsDeprecated)
				r.Get("/logs", api.workspaceAgentLogs)
				r.Get("/listening-ports", api.workspaceAgentListeningPorts)
				r.Get("/connection", api.workspaceAgentConnection)
				r.Get("/containers", api.workspaceAgentListContainers)
				r.Get("/containers/watch", api.watchWorkspaceAgentContainers)
				r.Delete("/containers/devcontainers/{devcontainer}", api.workspaceAgentDeleteDevcontainer)
				r.Post("/containers/devcontainers/{devcontainer}/recreate", api.workspaceAgentRecreateDevcontainer)
				r.Get("/coordinate", api.workspaceAgentClientCoordinate)

				// PTY is part of workspaceAppServer.
			})
		})
		r.Route("/workspaces", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
			)
			r.Get("/", api.workspaces)
			r.Route("/{workspace}", func(r chi.Router) {
				r.Use(
					httpmw.ExtractWorkspaceParam(options.Database),
				)
				r.Get("/", api.workspace)
				r.Patch("/", api.patchWorkspace)
				r.Route("/builds", func(r chi.Router) {
					r.Get("/", api.workspaceBuilds)
					r.Post("/", api.postWorkspaceBuilds)
				})
				r.Route("/autostart", func(r chi.Router) {
					r.Put("/", api.putWorkspaceAutostart)
				})
				r.Route("/ttl", func(r chi.Router) {
					r.Put("/", api.putWorkspaceTTL)
				})
				r.Get("/watch", api.watchWorkspaceSSE)
				r.Get("/watch-ws", api.watchWorkspaceWS)
				r.Put("/extend", api.putExtendWorkspace)
				r.Post("/usage", api.postWorkspaceUsage)
				r.Put("/dormant", api.putWorkspaceDormant)
				r.Put("/favorite", api.putFavoriteWorkspace)
				r.Delete("/favorite", api.deleteFavoriteWorkspace)
				r.Put("/autoupdates", api.putWorkspaceAutoupdates)
				r.Get("/resolve-autostart", api.resolveAutostart)
				r.Route("/port-share", func(r chi.Router) {
					r.Get("/", api.workspaceAgentPortShares)
					r.Post("/", api.postWorkspaceAgentPortShare)
					r.Delete("/", api.deleteWorkspaceAgentPortShare)
				})
				r.Get("/timings", api.workspaceTimings)
				r.Route("/acl", func(r chi.Router) {
					r.Get("/", api.workspaceACL)
					r.Patch("/", api.patchWorkspaceACL)
					r.Delete("/", api.deleteWorkspaceACL)
				})
				r.Get("/agent-connection-watch", api.workspaceAgentConnWatcher.WorkspaceAgentConnectionWatch)
			})
		})
		r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				httpmw.ExtractWorkspaceBuildParam(options.Database),
				httpmw.ExtractWorkspaceParam(options.Database),
			)
			r.Get("/", api.workspaceBuild)
			r.Patch("/cancel", api.patchCancelWorkspaceBuild)
			r.Post("/debug-events", api.postWorkspaceBuildDebugEvent)
			r.Get("/logs", api.workspaceBuildLogs)
			r.Get("/parameters", api.workspaceBuildParameters)
			r.Get("/resources", api.workspaceBuildResourcesDeprecated)
			r.Get("/secrets", api.workspaceBuildSecrets)
			r.Get("/state", api.workspaceBuildState)
			r.Put("/state", api.workspaceBuildUpdateState)
			r.Get("/timings", api.workspaceBuildTimings)
		})
		r.Route("/authcheck", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Post("/", api.checkAuthorization)
		})
		r.Route("/applications", func(r chi.Router) {
			r.Route("/host", func(r chi.Router) {
				// Don't leak the hostname to unauthenticated users.
				r.Use(apiKeyMiddleware)
				r.Get("/", api.appHost)
			})
			r.Route("/auth-redirect", func(r chi.Router) {
				// We want to redirect to login if they are not authenticated.
				r.Use(apiKeyMiddlewareRedirect)

				// This is a GET request as it's redirected to by the subdomain app
				// handler and the login page.
				r.Get("/", api.workspaceApplicationAuth)
			})
		})
		r.Route("/insights", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Group(func(r chi.Router) {
				r.Use(
					func(next http.Handler) http.Handler {
						return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
							// Template insights depend on the usage stats.
							if !options.DeploymentValues.StatsCollection.UsageStats.Enable.Value() {
								httpapi.Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{
									Message: "Not Found.",
									Detail:  "Template insights are disabled.",
								})
								return
							}

							next.ServeHTTP(rw, r)
						})
					},
				)
				r.Get("/daus", api.deploymentDAUs)
				r.Get("/user-activity", api.insightsUserActivity)
				r.Get("/user-latency", api.insightsUserLatency)
				r.Get("/templates", api.insightsTemplates)
			})
			r.Get("/user-status-counts", api.insightsUserStatusCounts)
		})
		r.Route("/debug", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				// Ensure only users with the debug_info:read (e.g. only owners)
				// can view debug endpoints.
				func(next http.Handler) http.Handler {
					return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
						if !api.Authorize(r, policy.ActionRead, rbac.ResourceDebugInfo) {
							httpapi.Forbidden(rw)
							return
						}

						next.ServeHTTP(rw, r)
					})
				},
			)

			r.Get("/coordinator", api.debugCoordinator)
			r.Get("/tailnet", api.debugTailnet)
			r.Route("/health", func(r chi.Router) {
				r.Get("/", api.debugDeploymentHealth)
				r.Route("/settings", func(r chi.Router) {
					r.Get("/", api.deploymentHealthSettings)
					r.Put("/", api.putDeploymentHealthSettings)
				})
			})
			r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP)
			r.Route("/{user}", func(r chi.Router) {
				r.Use(httpmw.ExtractUserParam(options.Database))
				r.Get("/debug-link", api.userDebugOIDC)
			})
			if options.DERPServer != nil {
				r.Route("/derp", func(r chi.Router) {
					r.Get("/traffic", options.DERPServer.ServeDebugTraffic)
				})
			}
			r.Method("GET", "/expvar", expvar.Handler()) // contains DERP metrics as well as cmdline and memstats

			r.Post("/profile", api.debugCollectProfile)

			r.Route("/pprof", func(r chi.Router) {
				r.Use(func(next http.Handler) http.Handler {
					// Some of the pprof handlers strip the `/debug/pprof`
					// prefix, so we need to strip our additional prefix as
					// well.
					return http.StripPrefix("/api/v2", next)
				})

				// Serve the index HTML page.
				r.Get("/", func(w http.ResponseWriter, r *http.Request) {
					// Redirect to include a trailing slash, otherwise links on
					// the generated HTML page will be broken.
					if !strings.HasSuffix(r.URL.Path, "/") {
						http.Redirect(w, r, "/api/v2/debug/pprof/", http.StatusTemporaryRedirect)
						return
					}
					httppprof.Index(w, r)
				})

				// Handle any out of the box pprof handlers that don't get
				// dealt with by the default index handler. See httppprof.init.
				r.Get("/cmdline", httppprof.Cmdline)
				r.Get("/profile", httppprof.Profile)
				r.Get("/symbol", httppprof.Symbol)
				r.Get("/trace", httppprof.Trace)

				// Index will handle any standard and custom runtime/pprof
				// profiles.
				r.Get("/*", httppprof.Index)
			})

			r.Get("/metrics", promhttp.InstrumentMetricHandler(
				options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
			).ServeHTTP)
		})
		// Manage OAuth2 applications that can use Coder as an OAuth2 provider.
		r.Route("/oauth2-provider", func(r chi.Router) {
			r.Use(
				apiKeyMiddleware,
				// POST /apps/{app}/secrets returns a plaintext client secret,
				// so this tree falls under the same RFC 6749 §5.1 requirement
				// as /oauth2. Settings carry no credential but share the
				// header; that is harmless.
				httpmw.NoStore,
			)
			r.Route("/apps", func(r chi.Router) {
				r.Use(httpmw.RequireOAuth2Provider(oauth2ProviderEnabled))
				r.Get("/", api.oAuth2ProviderApps())
				r.Post("/", api.postOAuth2ProviderApp())

				r.Route("/{app}", func(r chi.Router) {
					r.Use(httpmw.ExtractOAuth2ProviderApp(options.Database))
					r.Get("/", api.oAuth2ProviderApp())
					r.Put("/", api.putOAuth2ProviderApp())
					r.Delete("/", api.deleteOAuth2ProviderApp())

					r.Route("/secrets", func(r chi.Router) {
						r.Get("/", api.oAuth2ProviderAppSecrets())
						r.Post("/", api.postOAuth2ProviderAppSecret())

						r.Route("/{secretID}", func(r chi.Router) {
							r.Use(httpmw.ExtractOAuth2ProviderAppSecret(options.Database))
							r.Delete("/", api.deleteOAuth2ProviderAppSecret())
						})
					})
				})
			})
			// Deliberately not gated: settings stay reachable while the
			// provider is disabled so an admin can configure it before
			// turning it on.
			r.Route("/settings", func(r chi.Router) {
				r.Get("/", api.oauth2ProviderSettings)
				r.Put("/", api.putOAuth2ProviderSettings)
			})
		})
		r.Route("/notifications", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Route("/inbox", func(r chi.Router) {
				r.Get("/", api.listInboxNotifications)
				r.Put("/mark-all-as-read", api.markAllInboxNotificationsAsRead)
				r.Get("/watch", api.watchInboxNotifications)
				r.Put("/{id}/read-status", api.updateInboxNotificationReadStatus)
			})
			r.Get("/settings", api.notificationsSettings)
			r.Put("/settings", api.putNotificationsSettings)
			r.Route("/templates", func(r chi.Router) {
				r.Get("/system", api.systemNotificationTemplates)
				r.Get("/custom", api.customNotificationTemplates)
			})
			r.Get("/dispatch-methods", api.notificationDispatchMethods)
			r.Post("/test", api.postTestNotification)
			r.Post("/custom", api.postCustomNotification)
		})
		r.Route("/tailnet", func(r chi.Router) {
			r.Use(apiKeyMiddleware)
			r.Get("/", api.tailnetRPCConn)
		})
		r.Route("/init-script", func(r chi.Router) {
			r.Get("/{os}/{arch}", api.initScript)
		})
		r.Route("/ai/providers", aiProvidersHandler(api, apiKeyMiddleware))
	})

	if options.SwaggerEndpoint {
		// Swagger UI requires the URL trailing slash. Otherwise, the browser tries to load /assets
		// from http://localhost:8080/assets instead of http://localhost:8080/swagger/assets.
		r.Get("/swagger", http.RedirectHandler("/swagger/", http.StatusTemporaryRedirect).ServeHTTP)
		// See globalHTTPSwaggerHandler comment as to why we use a package
		// global variable here.
		r.Get("/swagger/*", globalHTTPSwaggerHandler)
	} else {
		swaggerDisabled := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
			httpapi.Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{
				Message: "Swagger documentation is disabled.",
			})
		})
		r.Get("/swagger", swaggerDisabled)
		r.Get("/swagger/*", swaggerDisabled)
	}

	additionalCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(api.DeploymentValues.AdditionalCSPPolicy))
	var cspParseErrors error
	for _, v := range api.DeploymentValues.AdditionalCSPPolicy {
		// Format is "<directive> <value> <value> ..."
		v = strings.TrimSpace(v)
		parts := strings.Split(v, " ")
		if len(parts) < 2 {
			cspParseErrors = errors.Join(cspParseErrors, xerrors.Errorf("invalid CSP header %q, not enough parts to be valid", v))
			continue
		}
		additionalCSPHeaders[httpmw.CSPFetchDirective(strings.ToLower(parts[0]))] = parts[1:]
	}

	if cspParseErrors != nil {
		// Do not fail Coder deployment startup because of this. Just log an error
		// and continue
		api.Logger.Error(context.Background(),
			"parsing additional CSP headers", slog.Error(cspParseErrors))
	}

	// Add blob: to img-src for chat file attachment previews.
	additionalCSPHeaders[httpmw.CSPDirectiveImgSrc] = append(
		additionalCSPHeaders[httpmw.CSPDirectiveImgSrc], "blob:",
	)
	// Add CSP headers to all static assets and pages. CSP headers only affect
	// browsers, so these don't make sense on api routes.
	cspProxyHosts := func() []*proxyhealth.ProxyHost {
		if api.DeploymentValues.Dangerous.AllowAllCors {
			// In this mode, allow all external requests.
			return []*proxyhealth.ProxyHost{
				{
					Host:    "*",
					AppHost: "*",
				},
			}
		}
		// Always add the primary, since the app host may be on a sub-domain.
		proxies := []*proxyhealth.ProxyHost{
			{
				Host:    api.AccessURL.Host,
				AppHost: appurl.ConvertAppHostForCSP(api.AccessURL.Host, api.AppHostname),
			},
		}
		if f := api.WorkspaceProxyHostsFn.Load(); f != nil {
			proxies = append(proxies, (*f)()...)
		}
		return proxies
	}
	cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), cspProxyHosts, additionalCSPHeaders)

	// Embed routes (e.g. VS Code extension chat) are designed to be
	// loaded inside iframes, so they must not include frame-ancestors
	// in their CSP. The CSP wildcard '*' only matches network schemes
	// (http, https, ws, wss) and cannot cover custom schemes like
	// vscode-webview://, so the only way to allow all embedders is
	// to omit the directive entirely. If the operator explicitly
	// configured frame-ancestors via CODER_ADDITIONAL_CSP_POLICY,
	// respect that setting.

	embedCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(additionalCSPHeaders))
	for k, v := range additionalCSPHeaders {
		embedCSPHeaders[k] = v
	}
	if _, ok := additionalCSPHeaders[httpmw.CSPFrameAncestors]; !ok {
		embedCSPHeaders[httpmw.CSPFrameAncestors] = []string{}
	}
	embedCSPMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), cspProxyHosts, embedCSPHeaders)
	embedHandler := embedCSPMW(compressHandler(httpmw.HSTS(api.SiteHandler, options.StrictTransportSecurityCfg)))
	r.Get("/agents/{agentId}/embed", embedHandler.ServeHTTP)
	r.Get("/agents/{agentId}/embed/*", embedHandler.ServeHTTP)

	// Static file handler must be wrapped with HSTS handler if the
	// StrictTransportSecurityAge is set. We only need to set this header on
	// static files since it only affects browsers.
	r.NotFound(cspMW(compressHandler(httpmw.HSTS(api.SiteHandler, options.StrictTransportSecurityCfg))).ServeHTTP)

	api.RootHandler = r

	return api
}

type API struct {
	// ctx is canceled immediately on shutdown, it can be used to abort
	// interruptible tasks.
	ctx    context.Context
	cancel context.CancelFunc

	// DeploymentID is loaded from the database on startup.
	DeploymentID string

	*Options
	mcpHTTPClient *http.Client
	// ID is a uniquely generated ID on initialization.
	// This is used to associate objects with a specific
	// Coder API instance, like workspace agents to a
	// specific replica.
	ID                                uuid.UUID
	Auditor                           atomic.Pointer[audit.Auditor]
	ConnectionLogger                  atomic.Pointer[connectionlog.ConnectionLogger]
	WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
	TailnetCoordinator                atomic.Pointer[tailnet.Coordinator]
	NetworkTelemetryBatcher           *tailnet.NetworkTelemetryBatcher
	TailnetClientService              *tailnet.ClientService
	// WebpushDispatcher is a way to send notifications to users via Web Push.
	WebpushDispatcher webpush.Dispatcher
	QuotaCommitter    atomic.Pointer[proto.QuotaCommitter]
	AppearanceFetcher atomic.Pointer[appearance.Fetcher]
	// WorkspaceProxyHostsFn returns the hosts of healthy workspace proxies
	// for header reasons.
	WorkspaceProxyHostsFn atomic.Pointer[func() []*proxyhealth.ProxyHost]
	// TemplateScheduleStore is a pointer to an atomic pointer because this is
	// passed to another struct, and we want them all to be the same reference.
	TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
	// UserQuietHoursScheduleStore is a pointer to an atomic pointer for the
	// same reason as TemplateScheduleStore.
	UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
	// DERPMapper mutates the DERPMap to include workspace proxies.
	DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
	// AccessControlStore is a pointer to an atomic pointer since it is
	// passed to dbauthz.
	AccessControlStore  *atomic.Pointer[dbauthz.AccessControlStore]
	PortSharer          atomic.Pointer[portsharing.PortSharer]
	FileCache           *files.Cache
	PrebuildsClaimer    atomic.Pointer[prebuilds.Claimer]
	PrebuildsReconciler atomic.Pointer[prebuilds.ReconciliationOrchestrator]
	// BuildUsageChecker is a pointer as it's passed around to multiple
	// components.
	BuildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker]
	// UsageInserter is a pointer to an atomic pointer because it is passed to
	// multiple components.
	UsageInserter *atomic.Pointer[usage.Inserter]
	// AIBridgeTransportFactory, when non-nil, lets chatd route LLM requests
	// through an in-process aibridge transport instead of calling upstream
	// providers directly. Registered by coderd at startup once aibridged is
	// wired in-memory.
	AIBridgeTransportFactory atomic.Pointer[aibridge.TransportFactory]
	// aiGatewayHandler is the in-memory AI Gateway HTTP handler
	// (no prefix stripping). Set by RegisterInMemoryAIBridgedHTTPHandler,
	// used by the enterprise /api/v2/aibridge and /api/v2/ai-gateway
	// routes (license-gated) which apply their own StripPrefix, and by
	// the in-memory transport (used by chatd, license-exempt).
	aiGatewayHandler http.Handler
	// AIGatewayServerMetrics records AI budget cost-control metrics. May be nil.
	AIGatewayServerMetrics *aibridgedserver.Metrics

	UpdatesProvider tailnet.WorkspaceUpdatesProvider

	HTTPAuth *HTTPAuthorizer

	// APIHandler serves "/api/v2"
	APIHandler chi.Router
	// ExperimentalHandler serves "/api/experimental"
	ExperimentalHandler chi.Router
	// RootHandler serves "/"
	RootHandler chi.Router

	// SiteHandler serves static files for the dashboard.
	SiteHandler *site.Handler

	WebsocketWaitMutex sync.Mutex
	WebsocketWaitGroup sync.WaitGroup
	derpCloseFunc      func()

	metricsCache          *metricscache.Cache
	updateChecker         *updatecheck.Checker
	WorkspaceAppsProvider workspaceapps.SignedTokenProvider
	workspaceAppServer    *workspaceapps.Server
	agentProvider         workspaceapps.AgentProvider

	// Experiments contains the list of experiments currently enabled.
	// This is used to gate features that are not yet ready for production.
	Experiments codersdk.Experiments
	// ExperimentEvaluator decides user-scoped experiments per user from
	// stored rules, falling back to Experiments. Use it instead of
	// Experiments for every experiment in codersdk.ExperimentsUserScoped.
	ExperimentEvaluator *experimentrules.Evaluator

	healthCheckGroup    *singleflight.Group[string, *healthsdk.HealthcheckReport]
	healthCheckCache    atomic.Pointer[healthsdk.HealthcheckReport]
	healthCheckProgress healthcheck.Progress

	statsReporter            *workspacestats.Reporter
	metadataBatcher          *metadatabatcher.Batcher
	lifecycleMetrics         *agentapi.LifecycleMetrics
	workspaceAgentRPCMetrics *WorkspaceAgentRPCMetrics
	wsWatcher                *httpapi.WSWatcher

	chatWorkspaceUploadsTotal *prometheus.CounterVec

	Acquirer *provisionerdserver.Acquirer
	// dbRolluper rolls up template usage stats from raw agent and app
	// stats. This is used to provide insights in the WebUI.
	dbRolluper *dbrollup.Rolluper
	// chatDaemon handles background processing of pending chats.
	chatDaemon *chatd.Server
	// chatLimits are the deployment chat limits shared by the chat daemon
	// and the chat HTTP handlers.
	chatLimits chatd.Limits
	// gitSyncWorker refreshes stale chat diff statuses in the background.
	gitSyncWorker *gitsync.Worker
	// AISeatTracker records AI seat usage.
	AISeatTracker aiseats.SeatTracker

	// ProfileCollector abstracts the runtime/pprof and runtime/trace
	// calls used by the /debug/profile endpoint. Tests override this
	// with a stub to avoid process-global side-effects.
	ProfileCollector ProfileCollector
	// ProfileCollecting is used as a concurrency guard so that only one
	// profile collection (via /debug/profile) can run at a time. The CPU
	// profiler is process-global, so concurrent collections would fail.
	ProfileCollecting atomic.Bool

	workspaceAgentConnWatcher  *workspaceconnwatcher.Watcher
	workspaceBuildOrchestrator *wsbuildorchestrator.Orchestrator
}

// chatDaemonPublishDiffStatusChangeFunc returns chatDaemon's
// PublishDiffStatusChange method bound as a gitsync.PublishDiffStatusChangeFunc,
// or a true nil func value when chatDaemon is nil (AI Gateway disabled).
//
// This must not be inlined as chatDaemon.PublishDiffStatusChange: a method
// value on a nil pointer receiver is itself non-nil (it captures the
// receiver, it doesn't call the method), so gitsync.Worker's own "if
// publishDiffStatusChangeFn != nil" check would not catch a nil chatDaemon,
// and invoking the returned func would panic dereferencing the nil
// receiver.
func chatDaemonPublishDiffStatusChangeFunc(chatDaemon *chatd.Server) gitsync.PublishDiffStatusChangeFunc {
	if chatDaemon == nil {
		return nil
	}
	return chatDaemon.PublishDiffStatusChange
}

// Close waits for all WebSocket connections to drain before returning.
func (api *API) Close() error {
	select {
	case <-api.ctx.Done():
		return xerrors.New("API already closed")
	default:
		api.cancel()
	}

	wsDone := make(chan struct{})
	timer := time.NewTimer(10 * time.Second)
	defer timer.Stop()
	go func() {
		api.WebsocketWaitMutex.Lock()
		defer api.WebsocketWaitMutex.Unlock()
		api.WebsocketWaitGroup.Wait()
		close(wsDone)
	}()
	// This will technically leak the above func if the timer fires, but this is
	// maintly a last ditch effort to un-stuck coderd on shutdown. This
	// shouldn't affect tests at all.
	select {
	case <-wsDone:
	case <-timer.C:
		api.Logger.Warn(api.ctx, "websocket shutdown timed out after 10 seconds")
	}
	api.dbRolluper.Close()
	// chatDiffWorker is unconditionally initialized in New().
	select {
	case <-api.gitSyncWorker.Done():
	case <-time.After(10 * time.Second):
		api.Logger.Warn(context.Background(),
			"chat diff refresh worker did not exit in time")
	}
	if api.chatDaemon != nil {
		if err := api.chatDaemon.Close(); err != nil {
			api.Logger.Warn(api.ctx, "close chat processor", slog.Error(err))
		}
	}
	api.metricsCache.Close()
	if api.updateChecker != nil {
		api.updateChecker.Close()
	}
	_ = api.workspaceAppServer.Close()
	_ = api.agentProvider.Close()
	if api.derpCloseFunc != nil {
		api.derpCloseFunc()
	}
	// The coordinator should be closed after the agent provider, and the DERP
	// handler.
	coordinator := api.TailnetCoordinator.Load()
	if coordinator != nil {
		_ = (*coordinator).Close()
	}
	_ = api.statsReporter.Close()
	if api.metadataBatcher != nil {
		api.metadataBatcher.Close()
	}
	_ = api.NetworkTelemetryBatcher.Close()
	_ = api.OIDCConvertKeyCache.Close()
	_ = api.ChatFileTokenKeyCache.Close()
	_ = api.AppSigningKeyCache.Close()
	_ = api.AppEncryptionKeyCache.Close()
	if api.NATSCACache != nil {
		_ = api.NATSCACache.Close()
	}
	_ = api.UpdatesProvider.Close()
	api.workspaceAgentConnWatcher.Close()
	api.workspaceBuildOrchestrator.Close()

	if current := api.PrebuildsReconciler.Load(); current != nil {
		ctx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop before shutdown"))
		defer giveUp()
		(*current).Stop(ctx, nil)
	}

	return nil
}

func compressHandler(h http.Handler) http.Handler {
	level := 5
	if flag.Lookup("test.v") != nil {
		level = 1
	}

	cmp := middleware.NewCompressor(level,
		"text/*",
		"application/*",
		"image/*",
	)
	for encoding := range site.StandardEncoders {
		writeCloserFn := site.StandardEncoders[encoding]
		cmp.SetEncoder(encoding, func(w io.Writer, level int) io.Writer {
			writeCloser := writeCloserFn(w, level)
			return writeCloser
		})
	}

	return cmp.Handler(h)
}

type MemoryProvisionerDaemonOption func(*memoryProvisionerDaemonOptions)

func MemoryProvisionerWithVersionOverride(version string) MemoryProvisionerDaemonOption {
	return func(opts *memoryProvisionerDaemonOptions) {
		opts.versionOverride = version
	}
}

func MemoryProvisionerWithHeartbeatOverride(heartbeatFN func(context.Context) error) MemoryProvisionerDaemonOption {
	return func(opts *memoryProvisionerDaemonOptions) {
		opts.heartbeatFn = heartbeatFN
	}
}

type memoryProvisionerDaemonOptions struct {
	versionOverride string
	heartbeatFn     func(context.Context) error
}

// CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd.
// Useful when starting coderd and provisionerd in the same process.
func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType) (client proto.DRPCProvisionerDaemonClient, err error) {
	return api.CreateInMemoryTaggedProvisionerDaemon(dialCtx, name, provisionerTypes, nil)
}

func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType, provisionerTags map[string]string, opts ...MemoryProvisionerDaemonOption) (client proto.DRPCProvisionerDaemonClient, err error) {
	options := &memoryProvisionerDaemonOptions{}
	for _, opt := range opts {
		opt(options)
	}

	tracer := api.TracerProvider.Tracer(tracing.TracerName)
	clientSession, serverSession := drpcsdk.MemTransportPipe()
	defer func() {
		if err != nil {
			_ = clientSession.Close()
			_ = serverSession.Close()
		}
	}()

	// All in memory provisioners will be part of the default org for now.
	//nolint:gocritic // in-memory provisioners are owned by system
	defaultOrg, err := api.Database.GetDefaultOrganization(dbauthz.AsSystemRestricted(dialCtx))
	if err != nil {
		return nil, xerrors.Errorf("unable to fetch default org for in memory provisioner: %w", err)
	}

	dbTypes := make([]database.ProvisionerType, 0, len(provisionerTypes))
	for _, tp := range provisionerTypes {
		dbTypes = append(dbTypes, database.ProvisionerType(tp))
	}

	keyID, err := uuid.Parse(string(codersdk.ProvisionerKeyIDBuiltIn))
	if err != nil {
		return nil, xerrors.Errorf("failed to parse built-in provisioner key ID: %w", err)
	}

	apiVersion := proto.CurrentVersion.String()
	if options.versionOverride != "" && flag.Lookup("test.v") != nil {
		// This should only be usable for unit testing. To fake a different provisioner version
		apiVersion = options.versionOverride
	}

	//nolint:gocritic // in-memory provisioners are owned by system
	daemon, err := api.Database.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(dialCtx), database.UpsertProvisionerDaemonParams{
		Name:           name,
		OrganizationID: defaultOrg.ID,
		CreatedAt:      dbtime.Now(),
		Provisioners:   dbTypes,
		Tags:           provisionersdk.MutateTags(uuid.Nil, provisionerTags),
		LastSeenAt:     sql.NullTime{Time: dbtime.Now(), Valid: true},
		Version:        buildinfo.Version(),
		APIVersion:     apiVersion,
		KeyID:          keyID,
	})
	if err != nil {
		return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err)
	}

	mux := drpcmux.New()
	api.Logger.Debug(dialCtx, "starting in-memory provisioner daemon", slog.F("name", name))
	logger := api.Logger.Named(fmt.Sprintf("inmem-provisionerd-%s", name))
	srv, err := provisionerdserver.NewServer(
		api.ctx, // use the same ctx as the API
		daemon.APIVersion,
		api.AccessURL,
		daemon.ID,
		defaultOrg.ID,
		logger,
		daemon.Provisioners,
		provisionerdserver.Tags(daemon.Tags),
		api.Database,
		api.Pubsub,
		api.Acquirer,
		api.Telemetry,
		tracer,
		&api.QuotaCommitter,
		&api.Auditor,
		api.TemplateScheduleStore,
		api.UserQuietHoursScheduleStore,
		api.UsageInserter,
		api.DeploymentValues,
		provisionerdserver.Options{
			OIDCConfig:          api.OIDCConfig,
			ExternalAuthConfigs: api.ExternalAuthConfigs,
			AISeatTracker:       api.AISeatTracker,
			Clock:               api.Clock,
			HeartbeatFn:         options.heartbeatFn,
		},
		api.NotificationsEnqueuer,
		&api.PrebuildsReconciler,
		api.ProvisionerdServerMetrics,
		api.Experiments,
	)
	if err != nil {
		return nil, err
	}
	err = proto.DRPCRegisterProvisionerDaemon(mux, srv)
	if err != nil {
		return nil, err
	}
	server := drpcsdk.NewServer(logger, &tracing.DRPCHandler{Handler: mux},
		drpcserver.Options{
			Manager: drpcsdk.DefaultDRPCOptions(nil),
			Log: func(err error) {
				if xerrors.Is(err, io.EOF) {
					return
				}
				logger.Debug(dialCtx, "drpc server error", slog.Error(err))
			},
		},
	)
	// in-mem pipes aren't technically "websockets" but they have the same properties as far as the
	// API is concerned: they are long-lived connections that we need to close before completing
	// shutdown of the API.
	api.WebsocketWaitMutex.Lock()
	api.WebsocketWaitGroup.Add(1)
	api.WebsocketWaitMutex.Unlock()
	go func() {
		defer api.WebsocketWaitGroup.Done()
		// here we pass the background context, since we want the server to keep serving until the
		// client hangs up.  If we, say, pass the API context, then when it is canceled, we could
		// drop a job that we locked in the database but never passed to the provisionerd.  The
		// provisionerd is local, in-mem, so there isn't a danger of losing contact with it and
		// having a dead connection we don't know the status of.
		err := server.Serve(context.Background(), serverSession)
		logger.Info(dialCtx, "provisioner daemon disconnected", slog.Error(err))
		// close the sessions, so we don't leak goroutines serving them.
		_ = clientSession.Close()
		_ = serverSession.Close()
	}()

	return proto.NewDRPCProvisionerDaemonClient(clientSession), nil
}

func (api *API) DERPMap() *tailcfg.DERPMap {
	fn := api.DERPMapper.Load()
	if fn != nil {
		return (*fn)(api.BaseDERPMap)
	}

	return api.BaseDERPMap
}

// nolint:revive
func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments {
	exps := make([]codersdk.Experiment, 0, len(raw))
	for _, v := range raw {
		switch v {
		case "*":
			exps = append(exps, codersdk.ExperimentsSafe...)
		default:
			ex := codersdk.Experiment(strings.ToLower(v))
			if !slice.Contains(codersdk.ExperimentsKnown, ex) {
				log.Warn(context.Background(), "ignoring unknown experiment", slog.F("experiment", ex))
			} else if !slice.Contains(codersdk.ExperimentsSafe, ex) {
				log.Warn(context.Background(), "🐉 HERE BE DRAGONS: opting into hidden experiment", slog.F("experiment", ex))
			}
			exps = append(exps, ex)
		}
	}
	return exps
}

var multipleSlashesRe = regexp.MustCompile(`/+`)

func singleSlashMW(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		var path string
		rctx := chi.RouteContext(r.Context())
		if rctx != nil && rctx.RoutePath != "" {
			path = rctx.RoutePath
		} else {
			path = r.URL.Path
		}

		// Normalize multiple slashes to a single slash
		newPath := multipleSlashesRe.ReplaceAllString(path, "/")

		// Apply the cleaned path
		// The approach is consistent with: https://github.com/go-chi/chi/blob/e846b8304c769c4f1a51c9de06bebfaa4576bd88/middleware/strip.go#L24-L28
		if rctx != nil {
			rctx.RoutePath = newPath
		} else {
			r.URL.Path = newPath
		}

		next.ServeHTTP(w, r)
	}
	return http.HandlerFunc(fn)
}