// Package config loads foreman's runtime configuration from environment variables. // // Why: centralizes all env-based configuration into a single, validated struct so // callers never read raw env vars directly. // What: reads FOREMAN_* environment variables, applies defaults, and validates // required values. // Test: set required env vars, call Load(), assert fields match; omit required vars, // assert Load() returns an error. package config import ( "fmt" "os" "strconv" "time" ) // Config holds all runtime configuration for the foreman daemon. type Config struct { // Addr is the listen address for the HTTP server (default ":8080"). Addr string // OllamaURL is the base URL of the Ollama target (required). OllamaURL string // OllamaToken is an optional bearer token sent to the Ollama target. OllamaToken string // Token is an optional bearer token that callers must present to foreman. Token string // EmbedModel is the always-resident embedder model name (e.g. "nomic-embed-text"). EmbedModel string // DBPath is the path to the SQLite database file (default "foreman.db"). DBPath string // PollInterval controls how often the model poller hits the target (default 30s). PollInterval time.Duration // WebhookSecret is an optional HMAC key for signing webhook payloads. WebhookSecret string // MaxAttempts is the maximum number of retry attempts for a job before it is // marked as failed (default 3). MaxAttempts int // JobTTL is how long terminal jobs are retained before the pruner deletes them // (default 24h). JobTTL time.Duration } // Load reads configuration from environment variables and returns a validated Config. // // Why: provides a single entry point for configuration with sensible defaults. // What: reads FOREMAN_* env vars, applies defaults, validates required fields. // Test: call with FOREMAN_OLLAMA_URL set, assert success; call without it, assert error. func Load() (Config, error) { cfg := Config{ Addr: envOr("FOREMAN_ADDR", ":8080"), OllamaURL: os.Getenv("FOREMAN_OLLAMA_URL"), OllamaToken: os.Getenv("FOREMAN_OLLAMA_TOKEN"), Token: os.Getenv("FOREMAN_TOKEN"), EmbedModel: os.Getenv("FOREMAN_EMBED_MODEL"), DBPath: envOr("FOREMAN_DB_PATH", "foreman.db"), WebhookSecret: os.Getenv("FOREMAN_WEBHOOK_SECRET"), } pollStr := envOr("FOREMAN_POLL_INTERVAL", "30s") dur, err := time.ParseDuration(pollStr) if err != nil { return Config{}, fmt.Errorf("invalid FOREMAN_POLL_INTERVAL %q: %w", pollStr, err) } cfg.PollInterval = dur maxAttemptsStr := envOr("FOREMAN_MAX_ATTEMPTS", "3") maxAttempts, err := strconv.Atoi(maxAttemptsStr) if err != nil { return Config{}, fmt.Errorf("invalid FOREMAN_MAX_ATTEMPTS %q: %w", maxAttemptsStr, err) } cfg.MaxAttempts = maxAttempts jobTTLStr := envOr("FOREMAN_JOB_TTL", "24h") jobTTL, err := time.ParseDuration(jobTTLStr) if err != nil { return Config{}, fmt.Errorf("invalid FOREMAN_JOB_TTL %q: %w", jobTTLStr, err) } cfg.JobTTL = jobTTL if cfg.OllamaURL == "" { return Config{}, fmt.Errorf("FOREMAN_OLLAMA_URL is required") } return cfg, nil } // envOr returns the value of the environment variable named by key, or fallback // if the variable is empty or unset. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }