// Package config loads, defaults, and validates the Cyrene v2 configuration // from environment variables and an optional .env file (development only). // // Every setting is sourced from an environment variable (no hard-coded values // in code). Nested groups map to struct fields tagged with `env:"NAME"`. // Missing required values cause Load/MustLoad to return an error so startup // can fail fast. package config import ( "fmt" "reflect" "strings" ) // Config is the root configuration tree. type Config struct { Env string `env:"ENV" default:"development"` LogLevel string `env:"LOG_LEVEL" default:"debug"` Timezone string `env:"TZ" default:"Asia/Shanghai"` DataDir string `env:"DATA_DIR" default:"./data"` AllowedOrigins []string `env:"ALLOWED_ORIGINS" default:"http://localhost:5173,http://localhost:3000"` Server ServerConfig Auth AuthConfig DB DBConfig LLM LLMConfig Embedding EmbeddingConfig IoT IoTConfig Voice VoiceConfig Background BackgroundConfig WSHub WSHubConfig ExecSandbox ExecSandbox MCP MCPConfig Capacity CapacityConfig } // ServerConfig holds the single gateway listener settings. type ServerConfig struct { Host string `env:"HOST" default:"0.0.0.0"` Port int `env:"PORT" default:"8080"` } // AuthConfig holds authentication and admin credential settings. type AuthConfig struct { JWTSecret string `env:"JWT_SECRET" validate:"required,min=32"` AdminUser string `env:"ADMIN_USERNAME" default:"admin"` AdminPass string `env:"ADMIN_PASSWORD" default:"admin12345"` RegistrationEnabled bool `env:"REGISTRATION_ENABLED" default:"true"` } // DBConfig holds database connection settings. SQLite is the default; PGURL is // the optional PostgreSQL+pgvector enhancement. type DBConfig struct { SQLitePath string `env:"DB_SQLITE_PATH" default:"./data/cyrene.db"` PGURL string `env:"DB_PG_URL"` } // LLMConfig holds the multi-supplier LLM adapter settings. Provider selects the // wire protocol; every provider has its own independent base/key/model/fallback // so multiple vendors can coexist. type LLMConfig struct { // Provider declares which protocol to use: "openai" (OpenAI-compatible // /chat/completions) or "anthropic" (Anthropic Messages /v1/messages). Provider string `env:"LLM_PROVIDER" default:"openai"` // OpenAI-compatible provider (default). Kept flat for backward compatibility // with the historic LLM_BASE_URL/API_KEY/MODEL/FALLBACK_MODEL env names. BaseURL string `env:"LLM_BASE_URL"` APIKey string `env:"LLM_API_KEY"` Model string `env:"LLM_MODEL"` Fallback string `env:"LLM_FALLBACK_MODEL"` // VisionModel is the dedicated model used for image/vision requests. When // set, the model router routes image content to it (see model.Router.WithVisionModel). VisionModel string `env:"LLM_VISION_MODEL"` // Anthropic native provider, used when Provider=anthropic. Independent // config block so each vendor is a self-contained config. AnthropicBaseURL string `env:"ANTHROPIC_BASE_URL"` AnthropicAPIKey string `env:"ANTHROPIC_API_KEY"` AnthropicModel string `env:"ANTHROPIC_MODEL"` AnthropicFallback string `env:"ANTHROPIC_FALLBACK_MODEL"` } // EmbeddingConfig holds the optional semantic embedding API settings. When // unset, the runtime falls back to built-in n-gram/FTS vectors. type EmbeddingConfig struct { APIURL string `env:"EMBEDDING_API_URL"` APIKey string `env:"EMBEDDING_API_KEY"` Model string `env:"EMBEDDING_MODEL"` } // IoTConfig selects the IoT device provider. type IoTConfig struct { Provider string `env:"IOT_PROVIDER" default:"mock"` } // VoiceConfig holds TTS/STT provider settings. type VoiceConfig struct { TTSProvider string `env:"TTS_PROVIDER" default:"edge-tts"` STTProvider string `env:"STT_PROVIDER" default:"dashscope"` DashscopeAPIKey string `env:"DASHSCOPE_API_KEY"` } // BackgroundConfig holds the autonomous thinking settings. type BackgroundConfig struct { Enabled bool `env:"ENABLE_BACKGROUND_THINKING" default:"true"` IntervalSec int `env:"THINK_INTERVAL_SEC" default:"300"` SilenceTimeout int `env:"THINK_SILENCE_TIMEOUT_SEC" default:"120"` } // WSHubConfig holds the WebSocket hub limits. type WSHubConfig struct { MaxConnections int `env:"WS_MAX_CONNECTIONS" default:"1000"` SessionIdleTimeout int `env:"SESSION_IDLE_TIMEOUT_MIN" default:"30"` } // MCPConfig holds Model Context Protocol server settings. The reflection-based // loader only supports scalars and []string, so servers are supplied as a JSON // array string (ServersJSON, env MCP_SERVERS_JSON) and parsed at startup by the // MCP wiring (cmd/cyrene/mcp.go). type MCPConfig struct { ServersJSON string `env:"MCP_SERVERS_JSON"` } // CapacityConfig configures the awakened-capacity decision layer // (DevDocs/design/14-唤醒式能力渐进.md §4). It is opt-in: the loop exposes every // tool until Enabled is true. type CapacityConfig struct { // Enabled turns on the capacity decider. When false the loop advertises every // registered tool (pre-existing behavior). Note: it is wired at the loop // periphery, not back-end (Invoke stays unblocked). Enabled bool `env:"CAPACITY_ENABLED" default:"false"` // MinConfidence is the wake threshold below which the state is forced toward // the persona default (设计 §2.3 默认保守). Zero selects the code default 0.6. MinConfidence float64 `env:"CAPACITY_MIN_CONFIDENCE" default:"0.6"` // ToolSetsJSON, when non-empty, is a JSON map of state→[]tool name, e.g. // {"quiet":["terminal","file"],"awake":["terminal","file","web-search"], // "full":null}. A missing state selects the built-in default set; null ("full") // means unrestricted. Parsed by the capacity wiring at startup (the // reflection-based loader only supports scalars and []string), matching the // MCP config pattern. ToolSetsJSON string `env:"CAPACITY_TOOL_SETS"` } // MCPServer describes a single MCP server: a stdio subprocess (Command/Args) or // an SSE endpoint (URL). Enabled gates activation at startup. type MCPServer struct { Name string `json:"name"` Command string `json:"command"` Args []string `json:"args"` URL string `json:"url"` Enabled bool `json:"enabled"` } // Load reads configuration, merging an optional .env file (development only) // with process environment variables. Process environment variables always take // precedence over .env values, which take precedence over code defaults. // // Passing an empty envPath skips .env entirely. A missing .env file is not an // error; only missing required values are. func Load(envPath string) (*Config, error) { fromEnv := map[string]string{} if envPath != "" { m, err := parseDotEnv(envPath) if err != nil { return nil, fmt.Errorf("config: read %s: %w", envPath, err) } fromEnv = m } cfg := &Config{} if err := decodeValue(reflect.ValueOf(cfg), fromEnv); err != nil { return nil, err } if errs := validateStruct(reflect.ValueOf(cfg).Elem()); len(errs) > 0 { parts := make([]string, 0, len(errs)) for _, e := range errs { parts = append(parts, e.Error()) } return nil, fmt.Errorf("config: validation failed: %s", strings.Join(parts, "; ")) } return cfg, nil } // MustLoad is Load that panics on error. It is intended for startup, matching // the "schema validation fails at boot" rule. func MustLoad(envPath string) *Config { cfg, err := Load(envPath) if err != nil { panic(err) } return cfg }