package config import ( "fmt" "log/slog" "os" "strings" ) // Config holds all runtime configuration for an Oikos role. // Hierarchy: compiled defaults → config file → env vars → Infisical (secrets only). type Config struct { // Database DatabaseURL string // postgres://user:pass@host:5432/oikos?sslmode=disable // API APIListen string // :8090 APIEnv string // dev, prod // MCP MCPBearerToken string // shared secret for Hermes→API MCP calls // Observability Debug bool // verbose logging, probe payloads, SQL // Seeds directory (for ingest/export) SeedsDir string // Migrations directory (embedded at build time, but path for fallback) MigrationsDir string } // Default returns a Config with compiled defaults. func Default() Config { return Config{ DatabaseURL: "postgres://oikos:oikos@localhost:5432/oikos?sslmode=disable", APIListen: ":8090", APIEnv: "dev", SeedsDir: "seeds", MigrationsDir: "migrations", } } // FromEnv loads config from environment variables, overlaying defaults. func FromEnv() Config { c := Default() if v := os.Getenv("OIKOS_DATABASE_URL"); v != "" { c.DatabaseURL = v } if v := os.Getenv("OIKOS_API_LISTEN"); v != "" { c.APIListen = v } if v := os.Getenv("OIKOS_ENV"); v != "" { c.APIEnv = v } if v := os.Getenv("OIKOS_MCP_BEARER_TOKEN"); v != "" { c.MCPBearerToken = v } if v := os.Getenv("OIKOS_SEEDS_DIR"); v != "" { c.SeedsDir = v } c.Debug = os.Getenv("OIKOS_DEBUG") == "true" || os.Getenv("OIKOS_DEBUG") == "1" return c } // redactedDBURL masks credentials in a postgres:// URL. func (c Config) redactedDBURL() string { dbURL := c.DatabaseURL if i := strings.Index(dbURL, "@"); i >= 0 { if j := strings.Index(dbURL, "://"); j >= 0 && j < i { dbURL = dbURL[:j+3] + "***" + dbURL[i:] } } return dbURL } // String returns a human-safe representation (secrets redacted). func (c Config) String() string { token := "" if c.MCPBearerToken != "" { token = "***" } return fmt.Sprintf("Config{DB=%s Listen=%s Env=%s Debug=%v MCPToken=%s SeedsDir=%s}", c.redactedDBURL(), c.APIListen, c.APIEnv, c.Debug, token, c.SeedsDir) } // LogValue implements slog.LogValuer so structured handlers (JSON) never // serialize raw secrets — without this, slog marshals struct fields // directly and String() is bypassed. func (c Config) LogValue() slog.Value { token := "" if c.MCPBearerToken != "" { token = "***" } return slog.GroupValue( slog.String("db", c.redactedDBURL()), slog.String("listen", c.APIListen), slog.String("env", c.APIEnv), slog.Bool("debug", c.Debug), slog.String("mcp_token", token), slog.String("seeds_dir", c.SeedsDir), ) }