// Package config loads runtime configuration for the CarrotAssistant backend. // // Configuration can be supplied via environment variables (preferred for // containers) and falls back to sensible defaults suitable for local // development. Secrets such as the master key MUST be provided in // production through the environment. package config import ( "fmt" "os" "path/filepath" "strings" ) // Config holds all runtime configuration values. type Config struct { // HTTPAddr is the address the API server listens on. HTTPAddr string // DataDir is the root directory for persistent files (sqlite db, // generated skill markdown, etc.). DataDir string // DBPath is the resolved SQLite database file path. DBPath string // SkillsDir is the root directory where skill markdown files live, // organised as //.md. SkillsDir string // MasterKey is used to encrypt secrets at rest (model api keys). It // must be 32 bytes; if a shorter value is supplied it is padded, if a // longer one is supplied it is truncated. Provide a strong value in // production via the MASTER_KEY env var. MasterKey string // JWTSecret signs admin session tokens. JWTSecret string // AdminBootstrapUser, if set and no admin user exists, is created on // first run. Useful for fresh deployments. AdminBootstrapUser string AdminBootstrapPassword string // DefaultAdminUser / DefaultAdminPassword are used when bootstrapping // is requested and no explicit credentials are configured. DefaultAdminUser string DefaultAdminPassword string } // Load reads configuration from environment variables with defaults. func Load() (*Config, error) { c := &Config{ HTTPAddr: env("HTTP_ADDR", ":8080"), DataDir: env("DATA_DIR", "./data"), MasterKey: env("MASTER_KEY", ""), JWTSecret: env("JWT_SECRET", ""), AdminBootstrapUser: env("ADMIN_BOOTSTRAP_USER", ""), AdminBootstrapPassword: env("ADMIN_BOOTSTRAP_PASSWORD", ""), DefaultAdminUser: env("DEFAULT_ADMIN_USER", "admin"), DefaultAdminPassword: env("DEFAULT_ADMIN_PASSWORD", "changeme"), } // Resolve derived paths to absolute form for stable behaviour in the // file loader and the HTTP handler layer. dataDir, err := filepath.Abs(c.DataDir) if err != nil { return nil, fmt.Errorf("resolve data dir: %w", err) } c.DataDir = dataDir c.DBPath = filepath.Join(dataDir, "carrot.db") c.SkillsDir = filepath.Join(dataDir, "skills") for _, dir := range []string{c.DataDir, c.SkillsDir} { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("mkdir %s: %w", dir, err) } } // In development we allow empty secrets and substitute a deterministic // value so the server still runs. A warning is printed to make the // insecure state obvious; production should always set these. if c.MasterKey == "" { c.MasterKey = "carrot-dev-master-key-please-change" } if c.JWTSecret == "" { c.JWTSecret = "carrot-dev-jwt-secret-please-change" } if len(c.MasterKey) > 32 { c.MasterKey = c.MasterKey[:32] } if len(c.MasterKey) < 32 { c.MasterKey = padKey(c.MasterKey, 32) } return c, nil } // env returns the value of the named env var, or fallback when unset/empty. func env(key, fallback string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v } return fallback } // padKey right-pads a short master key to the requested length. This only // exists so the dev defaults work; production must supply a real 32-byte key. func padKey(k string, n int) string { if len(k) >= n { return k[:n] } return k + strings.Repeat("0", n-len(k)) }