Files
story/internal/config/config.go
T

921 lines
42 KiB
Go

package config
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
EnvironmentLocal = "local"
EnvironmentTest = "test"
EnvironmentDev = "dev"
EnvironmentStage = "stage"
EnvironmentProd = "prod"
)
type Config struct {
VikingResourceID string
VikingCollectionName string
VikingAPIKey string
VikingEventType string
VikingProfileType string
ServiceName string
Environment string
LogLevel string
HTTPAddr string
DatabaseURL string
RedisAddr string
RedisPassword string
RedisDB int
AuthIssuerURL string
AuthJWKSURL string
AuthAudience string
OTLPEndpoint string
RateLimitRPS float64
RateLimitBurst int
RateLimitUserRPS float64
RateLimitUserBurst int
RateLimitTenantRPS float64
RateLimitTenantBurst int
RateLimitDeviceRPS float64
RateLimitDeviceBurst int
ProviderStartRPS float64
ProviderStartBurst int
ProviderMaxActiveTasks int
DeviceLocalSecret string
DeviceCredentialMasterKey string
ContentModelMasterKey string
DeviceClockSkew time.Duration
RTCAppID string
RTCAppKey string
RTCTokenIssuer string
RTCTokenSecret string
CursorSigningSecret string
ShareTokenSecret string
RTCTokenTTL time.Duration
AllocationTTL time.Duration
HeartbeatInterval int
ProviderCallbackSecret string
SubtitleCallbackURL string
SubtitleCallbackSecret string
SubtitlePrimary bool
ProviderCallbackSkew time.Duration
PlatformHealthSecret string
PlatformHealthSkew time.Duration
MockProviderBaseURL string
MockProviderAPIKey string
VoiceProvider string
VolcRTCBaseURL string
VolcAccessKeyID string
VolcSecretAccessKey string
VolcSessionToken string
VolcRTCRegion string
VolcRTCAPIVersion string
VolcVoiceChatTemplate string
VolcHardwareAgentBotID string
VolcHardwareTransport string
VolcHardwareMinimumSDKVersion string
VolcHardwareProvisioningEnabled bool
VolcHardwareInstanceID string
VolcHardwareProductKey string
VolcHardwareProductSecret string
VolcHardwareDeviceCredentialMasterKey string
VolcHardwareRegisterURL string
VolcHardwareRegisterTimeout time.Duration
VolcHardwareDeviceNamePrefix string
ProviderRequestTimeout time.Duration
ProviderFailureThreshold int
ProviderCircuitCooldown time.Duration
DependencyTimeout time.Duration
ExternalProbeInterval time.Duration
ShutdownTimeout time.Duration
WorkerPollInterval time.Duration
RecoveryScanInterval time.Duration
RecoveryStartingAfter time.Duration
RecoveryStoppingAfter time.Duration
ObjectStoreEndpoint string
ObjectStorePublic string
ObjectStoreAccessKey string
ObjectStoreSecretKey string
ObjectStoreBucket string
ObjectStoreRegion string
ObjectStoreSecure bool
ObjectStorePublicSecure bool
ExportDownloadTTL time.Duration
ExportObjectTTL time.Duration
MediaLocalDeleteGrace time.Duration
AlertWebhookURL string
AlertWebhookSecret string
AlertWebhookTimeout time.Duration
ContentGenerator string
ArkBaseURL string
ArkAPIKey string
ArkContentModel string
ArkRequestTimeout time.Duration
ContentSafetyProvider string
ContentSafetyEndpoint string
ContentSafetyAPIKey string
ContentSafetyPolicyRef string
ContentSafetyTimeout time.Duration
EmbeddingProvider string
ArkEmbeddingModel string
MemoryToolBaseURL string
MemoryToolSigningSecret string
MemoryToolTokenTTL time.Duration
MemoryToolRateRPS float64
MemoryToolRateBurst int
ProviderMemoryMode string
ProviderMemoryBaseURL string
ProviderMemoryAPIKey string
ProviderMemoryTimeout time.Duration
}
func Load(serviceName string) (Config, error) {
if strings.TrimSpace(serviceName) == "" {
return Config{}, errors.New("service name is required")
}
redisDB, err := intEnv("REDIS_DB", 0)
if err != nil {
return Config{}, err
}
dependencyTimeout, err := durationEnv("DEPENDENCY_TIMEOUT", 2*time.Second)
if err != nil {
return Config{}, err
}
externalProbeInterval, err := durationEnv("EXTERNAL_PROBE_INTERVAL", 30*time.Second)
if err != nil {
return Config{}, err
}
shutdownTimeout, err := durationEnv("SHUTDOWN_TIMEOUT", 10*time.Second)
if err != nil {
return Config{}, err
}
pollInterval, err := durationEnv("WORKER_POLL_INTERVAL", 2*time.Second)
if err != nil {
return Config{}, err
}
recoveryScanInterval, err := durationEnv("RECOVERY_SCAN_INTERVAL", 5*time.Second)
if err != nil {
return Config{}, err
}
recoveryStartingAfter, err := durationEnv("RECOVERY_STARTING_AFTER", 20*time.Second)
if err != nil {
return Config{}, err
}
recoveryStoppingAfter, err := durationEnv("RECOVERY_STOPPING_AFTER", 20*time.Second)
if err != nil {
return Config{}, err
}
deviceClockSkew, err := durationEnv("DEVICE_CLOCK_SKEW", 5*time.Minute)
if err != nil {
return Config{}, err
}
rtcTokenTTL, err := durationEnv("RTC_TOKEN_TTL", 5*time.Minute)
if err != nil {
return Config{}, err
}
allocationTTL, err := durationEnv("CONVERSATION_ALLOCATION_TTL", 30*time.Second)
if err != nil {
return Config{}, err
}
providerCallbackSkew, err := durationEnv("PROVIDER_CALLBACK_CLOCK_SKEW", 5*time.Minute)
if err != nil {
return Config{}, err
}
platformHealthSkew, err := durationEnv("PLATFORM_HEALTH_CALLBACK_CLOCK_SKEW", 5*time.Minute)
if err != nil {
return Config{}, err
}
providerRequestTimeout, err := durationEnv("PROVIDER_REQUEST_TIMEOUT", 5*time.Second)
if err != nil {
return Config{}, err
}
providerCircuitCooldown, err := durationEnv("PROVIDER_CIRCUIT_COOLDOWN", time.Minute)
if err != nil {
return Config{}, err
}
exportDownloadTTL, err := durationEnv("EXPORT_DOWNLOAD_TTL", 10*time.Minute)
if err != nil {
return Config{}, err
}
exportObjectTTL, err := durationEnv("EXPORT_OBJECT_TTL", 24*time.Hour)
if err != nil {
return Config{}, err
}
mediaLocalDeleteGrace, err := durationEnv("MEDIA_LOCAL_DELETE_GRACE", time.Hour)
if err != nil {
return Config{}, err
}
alertWebhookTimeout, err := durationEnv("ALERT_WEBHOOK_TIMEOUT", 5*time.Second)
if err != nil {
return Config{}, err
}
arkRequestTimeout, err := durationEnv("ARK_REQUEST_TIMEOUT", 30*time.Second)
if err != nil {
return Config{}, err
}
contentSafetyTimeout, err := durationEnv("CONTENT_SAFETY_TIMEOUT", 5*time.Second)
if err != nil {
return Config{}, err
}
providerMemoryTimeout, err := durationEnv("PROVIDER_MEMORY_TIMEOUT", 5*time.Second)
if err != nil {
return Config{}, err
}
memoryToolTokenTTL, err := durationEnv("MEMORY_TOOL_TOKEN_TTL", 4*time.Hour)
if err != nil {
return Config{}, err
}
volcHardwareProvisioningEnabled, err := boolEnv("VOLC_HARDWARE_PROVISIONING_ENABLED", false)
if err != nil {
return Config{}, err
}
volcHardwareRegisterTimeout, err := durationEnv("VOLC_HARDWARE_REGISTER_TIMEOUT", 10*time.Second)
if err != nil {
return Config{}, err
}
objectStoreSecure, err := boolEnv("OBJECT_STORE_SECURE", false)
if err != nil {
return Config{}, err
}
objectStorePublicSecure, err := boolEnv("OBJECT_STORE_PUBLIC_SECURE", objectStoreSecure)
if err != nil {
return Config{}, err
}
environment := stringEnv("APP_ENV", EnvironmentLocal)
secretValues, err := loadSecretValues(
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "DEVICE_CREDENTIAL_MASTER_KEY", "MODEL_CREDENTIAL_MASTER_KEY", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
"CURSOR_SIGNING_SECRET", "SHARE_TOKEN_SIGNING_SECRET", "PROVIDER_CALLBACK_SECRET", "PLATFORM_HEALTH_CALLBACK_SECRET",
"VOLC_SUBTITLE_CALLBACK_SECRET", "VIKING_MEMORY_API_KEY",
"MOCK_PROVIDER_API_KEY", "VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "VOLC_SESSION_TOKEN",
"VOLC_HARDWARE_PRODUCT_SECRET", "VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY",
"OBJECT_STORE_ACCESS_KEY", "OBJECT_STORE_SECRET_KEY", "ALERT_WEBHOOK_SECRET", "ARK_API_KEY", "CONTENT_SAFETY_API_KEY", "MEMORY_TOOL_SIGNING_SECRET", "PROVIDER_MEMORY_API_KEY",
)
if err != nil {
return Config{}, err
}
deviceLocalSecret := secretValues["DEVICE_LOCAL_SECRET"]
deviceCredentialMasterKey := secretValues["DEVICE_CREDENTIAL_MASTER_KEY"]
contentModelMasterKey := secretValues["MODEL_CREDENTIAL_MASTER_KEY"]
rtcTokenSecret := secretValues["RTC_TOKEN_SIGNING_SECRET"]
cursorSigningSecret := secretValues["CURSOR_SIGNING_SECRET"]
shareTokenSecret := secretValues["SHARE_TOKEN_SIGNING_SECRET"]
providerCallbackSecret := secretValues["PROVIDER_CALLBACK_SECRET"]
objectStoreEndpoint := strings.TrimSpace(os.Getenv("OBJECT_STORE_ENDPOINT"))
objectStorePublic := strings.TrimSpace(os.Getenv("OBJECT_STORE_PUBLIC_ENDPOINT"))
objectStoreAccessKey := secretValues["OBJECT_STORE_ACCESS_KEY"]
objectStoreSecretKey := secretValues["OBJECT_STORE_SECRET_KEY"]
objectStoreBucket := strings.TrimSpace(os.Getenv("OBJECT_STORE_EXPORT_BUCKET"))
objectStoreRegion := strings.TrimSpace(os.Getenv("OBJECT_STORE_REGION"))
if deviceLocalSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
deviceLocalSecret = "local-simulated-device-secret-32-bytes-minimum"
}
if deviceCredentialMasterKey == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
deviceCredentialMasterKey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
}
if contentModelMasterKey == "" && (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
contentModelMasterKey = "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
}
if rtcTokenSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
rtcTokenSecret = "local-simulated-rtc-token-secret-32-bytes-minimum"
}
if cursorSigningSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
cursorSigningSecret = "local-signed-pagination-cursor-secret-32-bytes-minimum"
}
if shareTokenSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
shareTokenSecret = "local-share-token-signing-secret-32-bytes-minimum"
}
memoryToolSigningSecret := secretValues["MEMORY_TOOL_SIGNING_SECRET"]
if memoryToolSigningSecret == "" && (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
memoryToolSigningSecret = "local-memory-tool-signing-secret-32-bytes-minimum"
}
if providerCallbackSecret == "" && serviceName == "callbacks" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
providerCallbackSecret = "local-provider-callback-secret-32-bytes-minimum"
}
if (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
if objectStoreEndpoint == "" {
objectStoreEndpoint = "localhost:59000"
}
if objectStorePublic == "" {
objectStorePublic = "localhost:59000"
}
if objectStoreAccessKey == "" {
objectStoreAccessKey = "local_minio"
}
if objectStoreSecretKey == "" {
objectStoreSecretKey = "local_minio_password"
}
if objectStoreBucket == "" {
objectStoreBucket = "private-exports"
}
if objectStoreRegion == "" {
objectStoreRegion = "us-east-1"
}
}
cfg := Config{
ServiceName: serviceName,
Environment: environment,
LogLevel: stringEnv("LOG_LEVEL", "info"),
HTTPAddr: stringEnv("HTTP_ADDR", defaultHTTPAddr(serviceName)),
DatabaseURL: secretDefault(secretValues["DATABASE_URL"], "postgres://voice:voice_dev_only@localhost:55432/voice_platform?sslmode=disable"),
RedisAddr: stringEnv("REDIS_ADDR", "localhost:56379"),
RedisPassword: secretValues["REDIS_PASSWORD"],
RedisDB: redisDB,
AuthIssuerURL: stringEnv("AUTH_ISSUER_URL", "http://localhost:58180/realms/voice-local"),
AuthJWKSURL: stringEnv("AUTH_JWKS_URL", "http://localhost:58180/realms/voice-local/protocol/openid-connect/certs"),
AuthAudience: stringEnv("AUTH_AUDIENCE", "voice-platform-web"),
OTLPEndpoint: stringEnvAllowEmpty("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:54317"),
DependencyTimeout: dependencyTimeout,
ExternalProbeInterval: externalProbeInterval,
ShutdownTimeout: shutdownTimeout,
WorkerPollInterval: pollInterval,
RecoveryScanInterval: recoveryScanInterval,
RecoveryStartingAfter: recoveryStartingAfter,
RecoveryStoppingAfter: recoveryStoppingAfter,
DeviceLocalSecret: deviceLocalSecret,
DeviceCredentialMasterKey: deviceCredentialMasterKey,
ContentModelMasterKey: contentModelMasterKey,
DeviceClockSkew: deviceClockSkew,
RTCAppID: stringEnv("RTC_APP_ID", "local-public-rtc-app"),
RTCAppKey: secretValues["RTC_APP_KEY"],
RTCTokenIssuer: stringEnv("RTC_TOKEN_ISSUER", "local"),
RTCTokenSecret: rtcTokenSecret,
CursorSigningSecret: cursorSigningSecret,
ShareTokenSecret: shareTokenSecret,
RTCTokenTTL: rtcTokenTTL,
AllocationTTL: allocationTTL,
ProviderCallbackSecret: providerCallbackSecret,
SubtitleCallbackURL: strings.TrimSpace(os.Getenv("VOLC_SUBTITLE_CALLBACK_URL")),
SubtitleCallbackSecret: secretValues["VOLC_SUBTITLE_CALLBACK_SECRET"],
SubtitlePrimary: os.Getenv("VOLC_SUBTITLE_PRIMARY") == "true",
ProviderCallbackSkew: providerCallbackSkew,
PlatformHealthSecret: secretValues["PLATFORM_HEALTH_CALLBACK_SECRET"],
PlatformHealthSkew: platformHealthSkew,
MockProviderBaseURL: stringEnv("MOCK_PROVIDER_BASE_URL", "http://localhost:59200"),
MockProviderAPIKey: secretDefault(secretValues["MOCK_PROVIDER_API_KEY"], "local-mock-provider-key"),
VoiceProvider: stringEnv("VOICE_PROVIDER", "mock"),
VolcRTCBaseURL: stringEnv("VOLC_RTC_BASE_URL", "https://rtc.volcengineapi.com"),
VolcAccessKeyID: secretValues["VOLC_ACCESS_KEY_ID"],
VolcSecretAccessKey: secretValues["VOLC_SECRET_ACCESS_KEY"],
VolcSessionToken: secretValues["VOLC_SESSION_TOKEN"],
VolcRTCRegion: stringEnv("VOLC_RTC_REGION", "cn-north-1"),
VolcRTCAPIVersion: stringEnv("VOLC_RTC_API_VERSION", "2024-12-01"),
VolcVoiceChatTemplate: strings.TrimSpace(os.Getenv("VOLC_VOICE_CHAT_TEMPLATE_FILE")),
VolcHardwareAgentBotID: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_AGENT_BOT_ID")),
VolcHardwareTransport: stringEnv("VOLC_HARDWARE_AGENT_TRANSPORT", "high_quality"),
VolcHardwareMinimumSDKVersion: stringEnv("VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION", "1.0.0"),
VolcHardwareProvisioningEnabled: volcHardwareProvisioningEnabled,
VolcHardwareInstanceID: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_INSTANCE_ID")),
VolcHardwareProductKey: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_PRODUCT_KEY")),
VolcHardwareProductSecret: secretValues["VOLC_HARDWARE_PRODUCT_SECRET"],
VolcHardwareDeviceCredentialMasterKey: secretValues["VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY"],
VolcHardwareRegisterURL: stringEnv("VOLC_HARDWARE_REGISTER_URL", "https://iot-cn-shanghai.iot.volces.com/2021-12-14/DynamicRegister?Action=DynamicRegister&Version=2021-12-14"),
VolcHardwareRegisterTimeout: volcHardwareRegisterTimeout,
VolcHardwareDeviceNamePrefix: stringEnv("VOLC_HARDWARE_DEVICE_NAME_PREFIX", "dt-"),
ProviderRequestTimeout: providerRequestTimeout,
ProviderCircuitCooldown: providerCircuitCooldown,
ObjectStoreEndpoint: objectStoreEndpoint,
ObjectStorePublic: objectStorePublic,
ObjectStoreAccessKey: objectStoreAccessKey,
ObjectStoreSecretKey: objectStoreSecretKey,
ObjectStoreBucket: objectStoreBucket,
ObjectStoreRegion: objectStoreRegion,
ObjectStoreSecure: objectStoreSecure,
ObjectStorePublicSecure: objectStorePublicSecure,
ExportDownloadTTL: exportDownloadTTL,
ExportObjectTTL: exportObjectTTL,
MediaLocalDeleteGrace: mediaLocalDeleteGrace,
AlertWebhookURL: strings.TrimSpace(os.Getenv("ALERT_WEBHOOK_URL")),
AlertWebhookSecret: secretValues["ALERT_WEBHOOK_SECRET"],
AlertWebhookTimeout: alertWebhookTimeout,
ContentGenerator: stringEnv("CONTENT_GENERATOR", "local"),
ArkBaseURL: stringEnv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"),
ArkAPIKey: secretValues["ARK_API_KEY"],
ArkContentModel: strings.TrimSpace(os.Getenv("ARK_CONTENT_MODEL")),
ArkRequestTimeout: arkRequestTimeout,
ContentSafetyProvider: stringEnv("CONTENT_SAFETY_PROVIDER", "local"),
ContentSafetyEndpoint: strings.TrimSpace(os.Getenv("CONTENT_SAFETY_ENDPOINT")),
ContentSafetyAPIKey: secretValues["CONTENT_SAFETY_API_KEY"],
ContentSafetyPolicyRef: strings.TrimSpace(os.Getenv("CONTENT_SAFETY_POLICY_REF")),
ContentSafetyTimeout: contentSafetyTimeout,
EmbeddingProvider: stringEnv("EMBEDDING_PROVIDER", "local"),
ArkEmbeddingModel: strings.TrimSpace(os.Getenv("ARK_EMBEDDING_MODEL")),
MemoryToolBaseURL: strings.TrimSpace(os.Getenv("MEMORY_TOOL_BASE_URL")),
MemoryToolSigningSecret: memoryToolSigningSecret,
MemoryToolTokenTTL: memoryToolTokenTTL,
VikingResourceID: strings.TrimSpace(os.Getenv("VIKING_MEMORY_RESOURCE_ID")),
VikingCollectionName: strings.TrimSpace(os.Getenv("VIKING_MEMORY_COLLECTION_NAME")),
VikingAPIKey: secretValues["VIKING_MEMORY_API_KEY"],
VikingEventType: stringEnv("VIKING_MEMORY_EVENT_TYPE", "event_v1"),
VikingProfileType: stringEnv("VIKING_MEMORY_PROFILE_TYPE", "profile_v1"),
ProviderMemoryMode: stringEnv("PROVIDER_MEMORY_MODE", "disabled"),
ProviderMemoryBaseURL: strings.TrimSpace(os.Getenv("PROVIDER_MEMORY_BASE_URL")),
ProviderMemoryAPIKey: secretValues["PROVIDER_MEMORY_API_KEY"],
ProviderMemoryTimeout: providerMemoryTimeout,
}
cfg.RateLimitRPS, err = floatEnv("RATE_LIMIT_RPS", 50)
if err != nil {
return Config{}, err
}
cfg.RateLimitBurst, err = intEnv("RATE_LIMIT_BURST", 100)
if err != nil {
return Config{}, err
}
cfg.RateLimitUserRPS, err = floatEnv("RATE_LIMIT_USER_RPS", 20)
if err != nil {
return Config{}, err
}
cfg.RateLimitUserBurst, err = intEnv("RATE_LIMIT_USER_BURST", 40)
if err != nil {
return Config{}, err
}
cfg.RateLimitTenantRPS, err = floatEnv("RATE_LIMIT_TENANT_RPS", 200)
if err != nil {
return Config{}, err
}
cfg.RateLimitTenantBurst, err = intEnv("RATE_LIMIT_TENANT_BURST", 400)
if err != nil {
return Config{}, err
}
cfg.RateLimitDeviceRPS, err = floatEnv("RATE_LIMIT_DEVICE_RPS", 10)
if err != nil {
return Config{}, err
}
cfg.RateLimitDeviceBurst, err = intEnv("RATE_LIMIT_DEVICE_BURST", 20)
if err != nil {
return Config{}, err
}
cfg.ProviderStartRPS, err = floatEnv("PROVIDER_START_RPS", 20)
if err != nil {
return Config{}, err
}
cfg.ProviderStartBurst, err = intEnv("PROVIDER_START_BURST", 40)
if err != nil {
return Config{}, err
}
cfg.ProviderMaxActiveTasks, err = intEnv("PROVIDER_MAX_ACTIVE_TASKS", 100)
if err != nil {
return Config{}, err
}
cfg.MemoryToolRateRPS, err = floatEnv("MEMORY_TOOL_RATE_RPS", 2)
if err != nil {
return Config{}, err
}
cfg.MemoryToolRateBurst, err = intEnv("MEMORY_TOOL_RATE_BURST", 5)
if err != nil {
return Config{}, err
}
cfg.HeartbeatInterval, err = intEnv("HEARTBEAT_INTERVAL_SECONDS", 15)
if err != nil {
return Config{}, err
}
cfg.ProviderFailureThreshold, err = intEnv("PROVIDER_FAILURE_THRESHOLD", 3)
if err != nil {
return Config{}, err
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
func (c Config) Validate() error {
if c.SubtitleCallbackURL != "" || c.SubtitleCallbackSecret != "" || c.SubtitlePrimary {
u, err := url.Parse(c.SubtitleCallbackURL)
if err != nil || u.Scheme != "https" || u.Host == "" || u.RawQuery != "" || u.Fragment != "" || u.User != nil || len(c.SubtitleCallbackSecret) < 32 {
return errors.New("native subtitle callback requires an HTTPS URL without query and a dedicated secret of at least 32 bytes")
}
}
validEnvironment := map[string]bool{
EnvironmentLocal: true,
EnvironmentTest: true,
EnvironmentDev: true,
EnvironmentStage: true,
EnvironmentProd: true,
}
if !validEnvironment[c.Environment] {
return fmt.Errorf("APP_ENV %q is not supported", c.Environment)
}
if strings.TrimSpace(c.HTTPAddr) == "" {
return errors.New("HTTP_ADDR is required")
}
if strings.TrimSpace(c.DatabaseURL) == "" {
return errors.New("DATABASE_URL is required")
}
if strings.TrimSpace(c.RedisAddr) == "" {
return errors.New("REDIS_ADDR is required")
}
if strings.TrimSpace(c.AuthIssuerURL) == "" || strings.TrimSpace(c.AuthJWKSURL) == "" || strings.TrimSpace(c.AuthAudience) == "" {
return errors.New("AUTH_ISSUER_URL, AUTH_JWKS_URL and AUTH_AUDIENCE are required")
}
if c.DependencyTimeout <= 0 || c.ExternalProbeInterval <= 0 || c.ShutdownTimeout <= 0 || c.WorkerPollInterval <= 0 ||
c.RecoveryScanInterval <= 0 || c.RecoveryStartingAfter <= 0 || c.RecoveryStoppingAfter <= 0 {
return errors.New("timeouts and worker poll interval must be positive")
}
if c.RateLimitRPS <= 0 || c.RateLimitBurst <= 0 ||
c.RateLimitUserRPS <= 0 || c.RateLimitUserBurst <= 0 ||
c.RateLimitTenantRPS <= 0 || c.RateLimitTenantBurst <= 0 ||
c.RateLimitDeviceRPS <= 0 || c.RateLimitDeviceBurst <= 0 ||
c.ProviderStartRPS <= 0 || c.ProviderStartBurst <= 0 || c.ProviderMaxActiveTasks <= 0 ||
c.MemoryToolRateRPS <= 0 || c.MemoryToolRateBurst <= 0 {
return errors.New("rate limits, bursts and provider task capacity must be positive")
}
if c.ServiceName == "api" {
decodedMasterKey, err := hex.DecodeString(c.DeviceCredentialMasterKey)
if err != nil || len(decodedMasterKey) != 32 {
return errors.New("DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
}
}
if c.ServiceName == "api" || c.ServiceName == "worker" {
decodedModelKey, err := hex.DecodeString(c.ContentModelMasterKey)
if err != nil || len(decodedModelKey) != 32 {
return errors.New("MODEL_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
}
}
if c.DeviceClockSkew <= 0 {
return errors.New("DEVICE_CLOCK_SKEW must be positive")
}
if c.ServiceName == "api" {
if c.RTCTokenIssuer != "local" && c.RTCTokenIssuer != "volcengine" {
return errors.New("RTC_TOKEN_ISSUER must be local or volcengine")
}
if c.RTCTokenIssuer == "local" && len(c.RTCTokenSecret) < 32 {
return errors.New("RTC_TOKEN_SIGNING_SECRET must contain at least 32 characters for the local issuer")
}
if c.RTCTokenIssuer == "volcengine" && len(c.RTCAppKey) < 16 {
return errors.New("RTC_APP_KEY must contain at least 16 characters for the Volcengine token issuer")
}
if c.RTCTokenIssuer == "volcengine" && len(c.RTCAppID) != 24 {
return errors.New("RTC_APP_ID must contain exactly 24 characters for the Volcengine token issuer")
}
}
if c.ServiceName == "api" && len(c.CursorSigningSecret) < 32 {
return errors.New("CURSOR_SIGNING_SECRET must contain at least 32 characters")
}
if c.ServiceName == "api" && len(c.ShareTokenSecret) < 32 {
return errors.New("SHARE_TOKEN_SIGNING_SECRET must contain at least 32 characters")
}
if (c.ServiceName == "api" || c.ServiceName == "worker") && (len(c.MemoryToolSigningSecret) < 32 || c.MemoryToolTokenTTL <= 0) {
return errors.New("MEMORY_TOOL_SIGNING_SECRET must contain at least 32 characters and token TTL must be positive")
}
if c.MemoryToolBaseURL != "" && !isAbsoluteHTTPURL(c.MemoryToolBaseURL) {
return errors.New("MEMORY_TOOL_BASE_URL must be an absolute HTTP(S) URL")
}
if c.ProviderMemoryMode != "disabled" && c.ProviderMemoryMode != "managed_http" {
return errors.New("PROVIDER_MEMORY_MODE must be disabled or managed_http")
}
if (c.ServiceName == "api" || c.ServiceName == "worker") && c.ProviderMemoryMode == "managed_http" && (!isAbsoluteHTTPURL(c.ProviderMemoryBaseURL) || c.ProviderMemoryAPIKey == "" || c.ProviderMemoryTimeout <= 0) {
return errors.New("managed provider memory requires an HTTP(S) base URL, API key and positive timeout")
}
if c.RTCAppID == "" || c.RTCTokenTTL <= 0 || c.AllocationTTL <= 0 || c.HeartbeatInterval <= 0 {
return errors.New("RTC app, token TTL, allocation TTL and heartbeat interval must be valid")
}
if c.ServiceName == "callbacks" && len(c.ProviderCallbackSecret) < 32 {
return errors.New("PROVIDER_CALLBACK_SECRET must contain at least 32 characters")
}
if c.ServiceName == "callbacks" && len(c.PlatformHealthSecret) < 32 {
return errors.New("PLATFORM_HEALTH_CALLBACK_SECRET must contain at least 32 characters")
}
if c.ServiceName == "worker" {
if c.VoiceProvider != "mock" && c.VoiceProvider != "volcengine" {
return errors.New("VOICE_PROVIDER must be mock or volcengine")
}
if c.VoiceProvider == "mock" && (c.MockProviderBaseURL == "" || c.MockProviderAPIKey == "") {
return errors.New("MOCK_PROVIDER_BASE_URL and MOCK_PROVIDER_API_KEY are required for the mock voice provider")
}
if c.VoiceProvider == "volcengine" {
if !isAbsoluteHTTPURL(c.VolcRTCBaseURL) || c.VolcAccessKeyID == "" || c.VolcSecretAccessKey == "" || c.VolcRTCRegion == "" || c.VolcRTCAPIVersion == "" || c.VolcVoiceChatTemplate == "" {
return errors.New("Volcengine voice provider requires an RTC URL, AK/SK, region, API version and voice-chat template file")
}
}
}
if c.ProviderCallbackSkew <= 0 || c.PlatformHealthSkew <= 0 || c.ProviderRequestTimeout <= 0 || c.ProviderFailureThreshold < 1 || c.ProviderCircuitCooldown <= 0 {
return errors.New("provider callback skew and request timeout must be positive")
}
if c.VolcHardwareTransport != "high_quality" && c.VolcHardwareTransport != "low_load" {
return errors.New("VOLC_HARDWARE_AGENT_TRANSPORT must be high_quality or low_load")
}
if c.ServiceName == "api" && c.VolcHardwareProvisioningEnabled {
providerKey, err := hex.DecodeString(c.VolcHardwareDeviceCredentialMasterKey)
if err != nil || len(providerKey) != 32 {
return errors.New("VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
}
if c.VolcHardwareInstanceID == "" || c.VolcHardwareProductKey == "" || c.VolcHardwareAgentBotID == "" ||
c.VolcHardwareMinimumSDKVersion == "" || len(c.VolcHardwareMinimumSDKVersion) > 80 ||
len(c.VolcHardwareProductSecret) < 16 || c.VolcHardwareRegisterTimeout <= 0 {
return errors.New("enabled hardware provisioning requires instance ID, product key, default Bot ID, minimum SDK, product secret and positive registration timeout")
}
}
if c.AlertWebhookTimeout <= 0 {
return errors.New("ALERT_WEBHOOK_TIMEOUT must be positive")
}
if c.ArkRequestTimeout <= 0 {
return errors.New("ARK_REQUEST_TIMEOUT must be positive")
}
if c.ContentSafetyTimeout <= 0 {
return errors.New("CONTENT_SAFETY_TIMEOUT must be positive")
}
if c.ServiceName == "worker" && c.AlertWebhookURL != "" {
parsed, err := url.Parse(c.AlertWebhookURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return errors.New("ALERT_WEBHOOK_URL must be an absolute HTTP(S) URL")
}
if len(c.AlertWebhookSecret) < 32 {
return errors.New("ALERT_WEBHOOK_SECRET must contain at least 32 characters when webhook delivery is enabled")
}
}
if c.ServiceName == "worker" {
if c.ContentSafetyProvider != "local" && c.ContentSafetyProvider != "managed_http" {
return errors.New("CONTENT_SAFETY_PROVIDER must be local or managed_http")
}
if c.ContentSafetyProvider == "managed_http" {
if !isAbsoluteHTTPURL(c.ContentSafetyEndpoint) || c.ContentSafetyAPIKey == "" || c.ContentSafetyPolicyRef == "" {
return errors.New("managed content safety requires an HTTP(S) endpoint, API key and policy reference")
}
}
if c.ContentGenerator != "local" && c.ContentGenerator != "ark" {
return errors.New("CONTENT_GENERATOR must be local or ark")
}
if c.ContentGenerator == "ark" {
parsed, err := url.Parse(c.ArkBaseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return errors.New("ARK_BASE_URL must be an absolute HTTP(S) URL")
}
if c.ArkAPIKey == "" || c.ArkContentModel == "" {
return errors.New("ARK_API_KEY and ARK_CONTENT_MODEL are required for the Ark content generator")
}
}
}
if c.ServiceName == "api" || c.ServiceName == "worker" {
if c.EmbeddingProvider != "local" && c.EmbeddingProvider != "ark" {
return errors.New("EMBEDDING_PROVIDER must be local or ark")
}
if c.EmbeddingProvider == "ark" {
parsed, err := url.Parse(c.ArkBaseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return errors.New("ARK_BASE_URL must be an absolute HTTP(S) URL")
}
if c.ArkAPIKey == "" || c.ArkEmbeddingModel == "" {
return errors.New("ARK_API_KEY and ARK_EMBEDDING_MODEL are required for the Ark embedding provider")
}
}
}
if c.ServiceName == "api" || c.ServiceName == "worker" {
if c.ObjectStoreEndpoint == "" || c.ObjectStorePublic == "" || c.ObjectStoreAccessKey == "" || c.ObjectStoreSecretKey == "" || c.ObjectStoreBucket == "" || c.ObjectStoreRegion == "" {
return errors.New("object store endpoint, public endpoint, credentials, region and export bucket are required")
}
if c.ExportDownloadTTL <= 0 || c.ExportDownloadTTL > 24*time.Hour || c.ExportObjectTTL <= 0 {
return errors.New("export download and object TTL must be positive, and download TTL cannot exceed 24 hours")
}
if c.MediaLocalDeleteGrace <= 0 || c.MediaLocalDeleteGrace > 7*24*time.Hour {
return errors.New("media local delete grace must be positive and cannot exceed 7 days")
}
}
if c.Environment == EnvironmentProd {
if err := c.validateProduction(); err != nil {
return err
}
}
return nil
}
func (c Config) validateProduction() error {
if !isHTTPSURL(c.AuthIssuerURL) || !isHTTPSURL(c.AuthJWKSURL) {
return errors.New("production OIDC issuer and JWKS URLs must use HTTPS")
}
databaseURL, err := url.Parse(c.DatabaseURL)
if err != nil || databaseURL.Scheme == "" || strings.EqualFold(databaseURL.Query().Get("sslmode"), "disable") {
return errors.New("production DATABASE_URL must enable transport security")
}
if strings.TrimSpace(c.RedisPassword) == "" || isPlaceholderSecret(c.RedisPassword) {
return errors.New("production REDIS_PASSWORD must be a non-placeholder secret")
}
if c.ServiceName == "api" {
if c.DeviceCredentialMasterKey == "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" {
return errors.New("production API must use a non-default DEVICE_CREDENTIAL_MASTER_KEY")
}
if isPlaceholderSecret(c.CursorSigningSecret) || isPlaceholderSecret(c.ShareTokenSecret) {
return errors.New("production API signing secrets must not use local or example values")
}
if c.CursorSigningSecret == c.ShareTokenSecret {
return errors.New("production API signing secrets must be distinct")
}
if isPlaceholderSecret(c.MemoryToolSigningSecret) || c.MemoryToolSigningSecret == c.CursorSigningSecret || c.MemoryToolSigningSecret == c.ShareTokenSecret {
return errors.New("production memory tool signing secret must be non-placeholder and distinct")
}
if c.MemoryToolBaseURL != "" && !isHTTPSURL(c.MemoryToolBaseURL) {
return errors.New("production memory tool URL must use HTTPS")
}
if isLocalValue(c.RTCAppID) || c.RTCTokenIssuer != "volcengine" || isPlaceholderSecret(c.RTCAppKey) {
return errors.New("production RTC credentials must use a real app ID, Volcengine token issuer and non-placeholder app key")
}
if c.EmbeddingProvider != "ark" || !isHTTPSURL(c.ArkBaseURL) || isPlaceholderSecret(c.ArkAPIKey) || isLocalValue(c.ArkEmbeddingModel) {
return errors.New("production memory retrieval must use Ark HTTPS, a non-placeholder API key and a real embedding model ID")
}
}
if c.ServiceName == "callbacks" && isPlaceholderSecret(c.ProviderCallbackSecret) {
return errors.New("production PROVIDER_CALLBACK_SECRET must not use a local or example value")
}
if c.ServiceName == "callbacks" && isPlaceholderSecret(c.PlatformHealthSecret) {
return errors.New("production PLATFORM_HEALTH_CALLBACK_SECRET must not use a local or example value")
}
if c.ServiceName == "worker" {
if c.VoiceProvider != "volcengine" || len(c.RTCAppID) != 24 || isLocalValue(c.RTCAppID) || !isHTTPSURL(c.VolcRTCBaseURL) || isLocalValue(c.VolcRTCBaseURL) || isPlaceholderSecret(c.VolcAccessKeyID) || isPlaceholderSecret(c.VolcSecretAccessKey) {
return errors.New("production voice provider must use signed Volcengine RTC HTTPS with non-placeholder AK/SK")
}
if c.AlertWebhookURL != "" && (!isHTTPSURL(c.AlertWebhookURL) || isPlaceholderSecret(c.AlertWebhookSecret)) {
return errors.New("production alert webhook must use HTTPS and a non-placeholder signing secret")
}
if c.ContentGenerator != "ark" || !isHTTPSURL(c.ArkBaseURL) || isPlaceholderSecret(c.ArkAPIKey) || isLocalValue(c.ArkContentModel) {
return errors.New("production content generation must use Ark HTTPS, a non-placeholder API key and a real model ID")
}
if c.ContentSafetyProvider != "managed_http" || !isHTTPSURL(c.ContentSafetyEndpoint) || isLocalValue(c.ContentSafetyEndpoint) || isPlaceholderSecret(c.ContentSafetyAPIKey) || isLocalValue(c.ContentSafetyPolicyRef) {
return errors.New("production content safety must use managed HTTPS with a non-placeholder API key and policy reference")
}
if c.EmbeddingProvider != "ark" || isLocalValue(c.ArkEmbeddingModel) {
return errors.New("production memory indexing must use a real Ark embedding model ID")
}
}
if c.ServiceName == "api" || c.ServiceName == "worker" {
if isPlaceholderSecret(c.MemoryToolSigningSecret) {
return errors.New("production memory tool signing secret must be non-placeholder")
}
if c.ProviderMemoryMode == "managed_http" && (!isHTTPSURL(c.ProviderMemoryBaseURL) || isLocalValue(c.ProviderMemoryBaseURL) || isPlaceholderSecret(c.ProviderMemoryAPIKey)) {
return errors.New("production provider memory must use managed HTTPS and a non-placeholder API key")
}
if !c.ObjectStoreSecure || !c.ObjectStorePublicSecure {
return errors.New("production object storage must use TLS")
}
if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) {
return errors.New("production object storage endpoints must not target localhost")
}
if isPlaceholderSecret(c.ObjectStoreAccessKey) || isPlaceholderSecret(c.ObjectStoreSecretKey) {
return errors.New("production object storage credentials must not use local or example values")
}
}
return nil
}
func isHTTPSURL(value string) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
return err == nil && strings.EqualFold(parsed.Scheme, "https") && parsed.Host != ""
}
func isAbsoluteHTTPURL(value string) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
return err == nil && (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) && parsed.Host != ""
}
func isLocalValue(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
allZeroes := normalized != "" && strings.Trim(normalized, "0") == ""
return strings.Contains(normalized, "localhost") || strings.Contains(normalized, "127.0.0.1") ||
strings.Contains(normalized, "mock-provider") || strings.Contains(normalized, "example.invalid") ||
strings.Contains(normalized, "replace-with") || strings.HasPrefix(normalized, "local-") ||
strings.HasPrefix(normalized, "local_") || allZeroes
}
func isPlaceholderSecret(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
return len(normalized) < 32 || strings.Contains(normalized, "local-") || strings.Contains(normalized, "local_") || strings.Contains(normalized, "dev_only") || strings.Contains(normalized, "changeme") || strings.Contains(normalized, "example")
}
func loadSecretValues(names ...string) (map[string]string, error) {
values := make(map[string]string, len(names))
for _, name := range names {
value, err := secretEnv(name)
if err != nil {
return nil, err
}
values[name] = value
}
return values, nil
}
func secretEnv(name string) (string, error) {
inline := strings.TrimSpace(os.Getenv(name))
filePath := strings.TrimSpace(os.Getenv(name + "_FILE"))
if inline != "" && filePath != "" {
return "", fmt.Errorf("%s and %s_FILE cannot both be configured", name, name)
}
if filePath == "" {
return inline, nil
}
if !filepath.IsAbs(filePath) {
return "", fmt.Errorf("%s_FILE must be an absolute path", name)
}
file, err := os.Open(filepath.Clean(filePath))
if err != nil {
return "", fmt.Errorf("read %s_FILE: secret file is unavailable", name)
}
defer file.Close()
info, err := file.Stat()
if err != nil || !info.Mode().IsRegular() {
return "", fmt.Errorf("read %s_FILE: secret path must be a regular file", name)
}
content, err := io.ReadAll(io.LimitReader(file, 64*1024+1))
if err != nil || len(content) > 64*1024 {
return "", fmt.Errorf("read %s_FILE: secret file exceeds 64 KiB", name)
}
content = bytes.TrimRight(content, "\r\n")
if bytes.IndexByte(content, 0) >= 0 {
return "", fmt.Errorf("read %s_FILE: secret contains a NUL byte", name)
}
return string(content), nil
}
func secretDefault(value, fallback string) string {
if value == "" {
return fallback
}
return value
}
func defaultHTTPAddr(serviceName string) string {
switch serviceName {
case "callbacks":
return "0.0.0.0:58081"
case "worker":
return "0.0.0.0:58082"
default:
return "0.0.0.0:58080"
}
}
func stringEnv(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func stringEnvAllowEmpty(key, fallback string) string {
value, exists := os.LookupEnv(key)
if !exists {
return fallback
}
return strings.TrimSpace(value)
}
func intEnv(key string, fallback int) (int, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%s must be an integer: %w", key, err)
}
return parsed, nil
}
func floatEnv(key string, fallback float64) (float64, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, fmt.Errorf("%s must be a number: %w", key, err)
}
return parsed, nil
}
func boolEnv(key string, fallback bool) (bool, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return false, fmt.Errorf("%s must be a boolean: %w", key, err)
}
return parsed, nil
}
func durationEnv(key string, fallback time.Duration) (time.Duration, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := time.ParseDuration(value)
if err != nil {
return 0, fmt.Errorf("%s must be a duration: %w", key, err)
}
return parsed, nil
}