package config import ( "fmt" "os" "time" toml "github.com/pelletier/go-toml/v2" ) // AgentConfig is the configuration for the mcp-agent daemon. type AgentConfig struct { Server ServerConfig `toml:"server"` Database DatabaseConfig `toml:"database"` MCIAS MCIASConfig `toml:"mcias"` Agent AgentSettings `toml:"agent"` MCProxy MCProxyConfig `toml:"mcproxy"` Metacrypt MetacryptConfig `toml:"metacrypt"` MCNS MCNSConfig `toml:"mcns"` Monitor MonitorConfig `toml:"monitor"` Log LogConfig `toml:"log"` } // MetacryptConfig holds the Metacrypt CA integration settings for // automated TLS cert provisioning. If ServerURL is empty, cert // provisioning is disabled. type MetacryptConfig struct { // ServerURL is the Metacrypt API base URL (e.g. "https://metacrypt:8443"). ServerURL string `toml:"server_url"` // CACert is the path to the CA certificate for verifying Metacrypt's TLS. CACert string `toml:"ca_cert"` // Mount is the CA engine mount name. Defaults to "pki". Mount string `toml:"mount"` // Issuer is the intermediate CA issuer name. Defaults to "infra". Issuer string `toml:"issuer"` // TokenPath is the path to the MCIAS service token file. TokenPath string `toml:"token_path"` } // MCNSConfig holds the MCNS DNS integration settings for automated // DNS record registration. If ServerURL is empty, DNS registration // is disabled. type MCNSConfig struct { // ServerURL is the MCNS API base URL (e.g. "https://localhost:28443"). ServerURL string `toml:"server_url"` // CACert is the path to the CA certificate for verifying MCNS's TLS. CACert string `toml:"ca_cert"` // TokenPath is the path to the MCIAS service token file. TokenPath string `toml:"token_path"` // Zone is the DNS zone for service records. Defaults to "svc.mcp.metacircular.net". Zone string `toml:"zone"` // NodeAddr is the IP address to register as the A record value. NodeAddr string `toml:"node_addr"` } // MCProxyConfig holds the mc-proxy connection settings. type MCProxyConfig struct { // Socket is the path to the mc-proxy gRPC admin API Unix socket. // If empty, route registration is disabled. Socket string `toml:"socket"` // CertDir is the directory containing TLS certificates for routes. // Convention: .pem and .key per service. // Defaults to /srv/mc-proxy/certs. CertDir string `toml:"cert_dir"` } // ServerConfig holds gRPC server listen address and TLS paths. type ServerConfig struct { GRPCAddr string `toml:"grpc_addr"` TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` } // DatabaseConfig holds the SQLite database path. type DatabaseConfig struct { Path string `toml:"path"` } // AgentSettings holds agent-specific settings. type AgentSettings struct { NodeName string `toml:"node_name"` ContainerRuntime string `toml:"container_runtime"` } // MonitorConfig holds monitoring loop parameters. type MonitorConfig struct { Interval Duration `toml:"interval"` AlertCommand []string `toml:"alert_command"` Cooldown Duration `toml:"cooldown"` FlapThreshold int `toml:"flap_threshold"` FlapWindow Duration `toml:"flap_window"` Retention Duration `toml:"retention"` } // LogConfig holds logging settings. type LogConfig struct { Level string `toml:"level"` } // Duration wraps time.Duration to support TOML string unmarshaling. // It accepts Go duration strings (e.g. "60s", "15m", "24h") plus a // "d" suffix for days (e.g. "30d" becomes 30*24h). type Duration struct { time.Duration } // UnmarshalText implements encoding.TextUnmarshaler for TOML parsing. func (d *Duration) UnmarshalText(text []byte) error { s := string(text) if s == "" { d.Duration = 0 return nil } // Handle "d" suffix for days. if len(s) > 1 && s[len(s)-1] == 'd' { var days float64 if _, err := fmt.Sscanf(s[:len(s)-1], "%f", &days); err != nil { return fmt.Errorf("parse duration %q: %w", s, err) } d.Duration = time.Duration(days * float64(24*time.Hour)) return nil } dur, err := time.ParseDuration(s) if err != nil { return fmt.Errorf("parse duration %q: %w", s, err) } d.Duration = dur return nil } // MarshalText implements encoding.TextMarshaler for TOML serialization. func (d Duration) MarshalText() ([]byte, error) { return []byte(d.String()), nil } // LoadAgentConfig reads and validates an agent configuration file. // Environment variables override file values for select fields. func LoadAgentConfig(path string) (*AgentConfig, error) { data, err := os.ReadFile(path) //nolint:gosec // config path from trusted CLI flag if err != nil { return nil, fmt.Errorf("read config %q: %w", path, err) } var cfg AgentConfig if err := toml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config %q: %w", path, err) } applyAgentDefaults(&cfg) applyAgentEnvOverrides(&cfg) if err := validateAgentConfig(&cfg); err != nil { return nil, fmt.Errorf("validate config: %w", err) } return &cfg, nil } func applyAgentDefaults(cfg *AgentConfig) { if cfg.Monitor.Interval.Duration == 0 { cfg.Monitor.Interval.Duration = 60 * time.Second } if cfg.Monitor.Cooldown.Duration == 0 { cfg.Monitor.Cooldown.Duration = 15 * time.Minute } if cfg.Monitor.FlapThreshold == 0 { cfg.Monitor.FlapThreshold = 3 } if cfg.Monitor.FlapWindow.Duration == 0 { cfg.Monitor.FlapWindow.Duration = 10 * time.Minute } if cfg.Monitor.Retention.Duration == 0 { cfg.Monitor.Retention.Duration = 30 * 24 * time.Hour // 30 days } if cfg.Log.Level == "" { cfg.Log.Level = "info" } if cfg.Agent.ContainerRuntime == "" { cfg.Agent.ContainerRuntime = "podman" } if cfg.MCProxy.CertDir == "" { cfg.MCProxy.CertDir = "/srv/mc-proxy/certs" } if cfg.Metacrypt.Mount == "" { cfg.Metacrypt.Mount = "pki" } if cfg.Metacrypt.Issuer == "" { cfg.Metacrypt.Issuer = "infra" } if cfg.MCNS.Zone == "" { cfg.MCNS.Zone = "svc.mcp.metacircular.net" } } func applyAgentEnvOverrides(cfg *AgentConfig) { if v := os.Getenv("MCP_AGENT_SERVER_GRPC_ADDR"); v != "" { cfg.Server.GRPCAddr = v } if v := os.Getenv("MCP_AGENT_SERVER_TLS_CERT"); v != "" { cfg.Server.TLSCert = v } if v := os.Getenv("MCP_AGENT_SERVER_TLS_KEY"); v != "" { cfg.Server.TLSKey = v } if v := os.Getenv("MCP_AGENT_DATABASE_PATH"); v != "" { cfg.Database.Path = v } if v := os.Getenv("MCP_AGENT_NODE_NAME"); v != "" { cfg.Agent.NodeName = v } if v := os.Getenv("MCP_AGENT_CONTAINER_RUNTIME"); v != "" { cfg.Agent.ContainerRuntime = v } if v := os.Getenv("MCP_AGENT_LOG_LEVEL"); v != "" { cfg.Log.Level = v } if v := os.Getenv("MCP_AGENT_MCPROXY_SOCKET"); v != "" { cfg.MCProxy.Socket = v } if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" { cfg.MCProxy.CertDir = v } if v := os.Getenv("MCP_AGENT_METACRYPT_SERVER_URL"); v != "" { cfg.Metacrypt.ServerURL = v } if v := os.Getenv("MCP_AGENT_METACRYPT_TOKEN_PATH"); v != "" { cfg.Metacrypt.TokenPath = v } if v := os.Getenv("MCP_AGENT_MCNS_SERVER_URL"); v != "" { cfg.MCNS.ServerURL = v } if v := os.Getenv("MCP_AGENT_MCNS_TOKEN_PATH"); v != "" { cfg.MCNS.TokenPath = v } if v := os.Getenv("MCP_AGENT_MCNS_NODE_ADDR"); v != "" { cfg.MCNS.NodeAddr = v } } func validateAgentConfig(cfg *AgentConfig) error { if cfg.Server.GRPCAddr == "" { return fmt.Errorf("server.grpc_addr is required") } if cfg.Server.TLSCert == "" { return fmt.Errorf("server.tls_cert is required") } if cfg.Server.TLSKey == "" { return fmt.Errorf("server.tls_key is required") } if cfg.Database.Path == "" { return fmt.Errorf("database.path is required") } if cfg.MCIAS.ServerURL == "" { return fmt.Errorf("mcias.server_url is required") } if cfg.MCIAS.ServiceName == "" { return fmt.Errorf("mcias.service_name is required") } if cfg.Agent.NodeName == "" { return fmt.Errorf("agent.node_name is required") } return nil }