package config import ( "fmt" "net/url" "os" "strings" "time" ) type Config struct { Server ServerConfig `envconfig:"server"` Database DatabaseConfig `envconfig:"database"` GITEA GITEAConfig `envconfig:"gitea"` Auth AuthConfig `envconfig:"auth"` Logging LoggingConfig `envconfig:"logging"` BACKBEAT BackbeatConfig `envconfig:"backbeat"` Docker DockerConfig `envconfig:"docker"` N8N N8NConfig `envconfig:"n8n"` OpenTelemetry OpenTelemetryConfig `envconfig:"opentelemetry"` Composer ComposerConfig `envconfig:"composer"` } type ServerConfig struct { ListenAddr string `envconfig:"LISTEN_ADDR" default:":8080"` ReadTimeout time.Duration `envconfig:"READ_TIMEOUT" default:"30s"` WriteTimeout time.Duration `envconfig:"WRITE_TIMEOUT" default:"30s"` ShutdownTimeout time.Duration `envconfig:"SHUTDOWN_TIMEOUT" default:"30s"` AllowedOrigins []string `envconfig:"ALLOWED_ORIGINS" default:"http://localhost:3000,http://localhost:8080"` AllowedOriginsFile string `envconfig:"ALLOWED_ORIGINS_FILE"` } type DatabaseConfig struct { Host string `envconfig:"DB_HOST" default:"localhost"` Port int `envconfig:"DB_PORT" default:"5432"` Database string `envconfig:"DB_NAME" default:"whoosh"` Username string `envconfig:"DB_USER" default:"whoosh"` Password string `envconfig:"DB_PASSWORD"` PasswordFile string `envconfig:"DB_PASSWORD_FILE"` SSLMode string `envconfig:"DB_SSL_MODE" default:"disable"` URL string `envconfig:"DB_URL"` AutoMigrate bool `envconfig:"DB_AUTO_MIGRATE" default:"false"` MaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"25"` MaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"5"` } type GITEAConfig struct { BaseURL string `envconfig:"BASE_URL" required:"true"` Token string `envconfig:"TOKEN"` TokenFile string `envconfig:"TOKEN_FILE"` WebhookPath string `envconfig:"WEBHOOK_PATH" default:"/webhooks/gitea"` WebhookToken string `envconfig:"WEBHOOK_TOKEN"` WebhookTokenFile string `envconfig:"WEBHOOK_TOKEN_FILE"` // Fetch hardening options EagerFilter bool `envconfig:"EAGER_FILTER" default:"true"` // Pre-filter by labels at API level FullRescan bool `envconfig:"FULL_RESCAN" default:"false"` // Ignore since parameter for full rescan DebugURLs bool `envconfig:"DEBUG_URLS" default:"false"` // Log exact URLs being used MaxRetries int `envconfig:"MAX_RETRIES" default:"3"` // Maximum retry attempts RetryDelay time.Duration `envconfig:"RETRY_DELAY" default:"2s"` // Delay between retries } type AuthConfig struct { JWTSecret string `envconfig:"JWT_SECRET"` JWTSecretFile string `envconfig:"JWT_SECRET_FILE"` JWTExpiry time.Duration `envconfig:"JWT_EXPIRY" default:"24h"` ServiceTokens []string `envconfig:"SERVICE_TOKENS"` ServiceTokensFile string `envconfig:"SERVICE_TOKENS_FILE"` } type LoggingConfig struct { Level string `envconfig:"LEVEL" default:"info"` Environment string `envconfig:"ENVIRONMENT" default:"production"` } type BackbeatConfig struct { Enabled bool `envconfig:"ENABLED" default:"true"` ClusterID string `envconfig:"CLUSTER_ID" default:"chorus-production"` AgentID string `envconfig:"AGENT_ID" default:"whoosh"` NATSUrl string `envconfig:"NATS_URL" default:"nats://backbeat-nats:4222"` } type DockerConfig struct { Enabled bool `envconfig:"ENABLED" default:"true"` Host string `envconfig:"HOST" default:"unix:///var/run/docker.sock"` } type N8NConfig struct { BaseURL string `envconfig:"BASE_URL" default:"https://n8n.home.deepblack.cloud"` } type OpenTelemetryConfig struct { Enabled bool `envconfig:"ENABLED" default:"true"` ServiceName string `envconfig:"SERVICE_NAME" default:"whoosh"` ServiceVersion string `envconfig:"SERVICE_VERSION" default:"0.1.7"` Environment string `envconfig:"ENVIRONMENT" default:"production"` JaegerEndpoint string `envconfig:"JAEGER_ENDPOINT" default:"http://localhost:14268/api/traces"` SampleRate float64 `envconfig:"SAMPLE_RATE" default:"1.0"` } type ComposerConfig struct { // Feature flags for experimental features EnableLLMClassification bool `envconfig:"ENABLE_LLM_CLASSIFICATION" default:"false"` EnableLLMSkillAnalysis bool `envconfig:"ENABLE_LLM_SKILL_ANALYSIS" default:"false"` EnableLLMTeamMatching bool `envconfig:"ENABLE_LLM_TEAM_MATCHING" default:"false"` // Analysis features EnableComplexityAnalysis bool `envconfig:"ENABLE_COMPLEXITY_ANALYSIS" default:"true"` EnableRiskAssessment bool `envconfig:"ENABLE_RISK_ASSESSMENT" default:"true"` EnableAlternativeOptions bool `envconfig:"ENABLE_ALTERNATIVE_OPTIONS" default:"false"` // Debug and monitoring EnableAnalysisLogging bool `envconfig:"ENABLE_ANALYSIS_LOGGING" default:"true"` EnablePerformanceMetrics bool `envconfig:"ENABLE_PERFORMANCE_METRICS" default:"true"` EnableFailsafeFallback bool `envconfig:"ENABLE_FAILSAFE_FALLBACK" default:"true"` // LLM model configuration ClassificationModel string `envconfig:"CLASSIFICATION_MODEL" default:"llama3.1:8b"` SkillAnalysisModel string `envconfig:"SKILL_ANALYSIS_MODEL" default:"llama3.1:8b"` MatchingModel string `envconfig:"MATCHING_MODEL" default:"llama3.1:8b"` // Performance settings AnalysisTimeoutSecs int `envconfig:"ANALYSIS_TIMEOUT_SECS" default:"60"` SkillMatchThreshold float64 `envconfig:"SKILL_MATCH_THRESHOLD" default:"0.6"` } func readSecretFile(filePath string) (string, error) { if filePath == "" { return "", nil } content, err := os.ReadFile(filePath) if err != nil { return "", fmt.Errorf("failed to read secret file %s: %w", filePath, err) } return strings.TrimSpace(string(content)), nil } func (c *Config) loadSecrets() error { // Load database password from file if specified if c.Database.PasswordFile != "" { password, err := readSecretFile(c.Database.PasswordFile) if err != nil { return err } c.Database.Password = password } // Load GITEA token from file if specified if c.GITEA.TokenFile != "" { token, err := readSecretFile(c.GITEA.TokenFile) if err != nil { return err } c.GITEA.Token = token } // Load GITEA webhook token from file if specified if c.GITEA.WebhookTokenFile != "" { token, err := readSecretFile(c.GITEA.WebhookTokenFile) if err != nil { return err } c.GITEA.WebhookToken = token } // Load JWT secret from file if specified if c.Auth.JWTSecretFile != "" { secret, err := readSecretFile(c.Auth.JWTSecretFile) if err != nil { return err } c.Auth.JWTSecret = secret } // Load service tokens from file if specified if c.Auth.ServiceTokensFile != "" { tokens, err := readSecretFile(c.Auth.ServiceTokensFile) if err != nil { return err } c.Auth.ServiceTokens = strings.Split(tokens, ",") // Trim whitespace from each token for i, token := range c.Auth.ServiceTokens { c.Auth.ServiceTokens[i] = strings.TrimSpace(token) } } // Load allowed origins from file if specified if c.Server.AllowedOriginsFile != "" { origins, err := readSecretFile(c.Server.AllowedOriginsFile) if err != nil { return err } c.Server.AllowedOrigins = strings.Split(origins, ",") // Trim whitespace from each origin for i, origin := range c.Server.AllowedOrigins { c.Server.AllowedOrigins[i] = strings.TrimSpace(origin) } } return nil } func (c *Config) Validate() error { // Load secrets from files first if err := c.loadSecrets(); err != nil { return err } // Validate required database password if c.Database.Password == "" { return fmt.Errorf("database password is required (set WHOOSH_DATABASE_DB_PASSWORD or WHOOSH_DATABASE_DB_PASSWORD_FILE)") } // Build database URL if not provided if c.Database.URL == "" { c.Database.URL = fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", url.QueryEscape(c.Database.Username), url.QueryEscape(c.Database.Password), c.Database.Host, c.Database.Port, url.QueryEscape(c.Database.Database), c.Database.SSLMode, ) } if c.GITEA.BaseURL == "" { return fmt.Errorf("GITEA base URL is required") } if c.GITEA.Token == "" { return fmt.Errorf("GITEA token is required (set WHOOSH_GITEA_TOKEN or WHOOSH_GITEA_TOKEN_FILE)") } if c.GITEA.WebhookToken == "" { return fmt.Errorf("GITEA webhook token is required (set WHOOSH_GITEA_WEBHOOK_TOKEN or WHOOSH_GITEA_WEBHOOK_TOKEN_FILE)") } if c.Auth.JWTSecret == "" { return fmt.Errorf("JWT secret is required (set WHOOSH_AUTH_JWT_SECRET or WHOOSH_AUTH_JWT_SECRET_FILE)") } if len(c.Auth.ServiceTokens) == 0 { return fmt.Errorf("at least one service token is required (set WHOOSH_AUTH_SERVICE_TOKENS or WHOOSH_AUTH_SERVICE_TOKENS_FILE)") } return nil }