From 78890ed76af80a5936fea46859f8801272c879c6 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 2 Apr 2026 15:23:19 -0700 Subject: [PATCH] Add master config loader MasterConfig with TOML loading, env overrides (MCP_MASTER_*), defaults, and validation. Follows the exact pattern of AgentConfig. Includes: server, database, MCIAS, edge (allowed_domains), registration (allowed_agents, max_nodes), timeouts, MCNS, bootstrap [[nodes]], and master service token path. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/config/master.go | 168 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 internal/config/master.go diff --git a/internal/config/master.go b/internal/config/master.go new file mode 100644 index 0000000..231ac12 --- /dev/null +++ b/internal/config/master.go @@ -0,0 +1,168 @@ +package config + +import ( + "fmt" + "os" + "time" + + toml "github.com/pelletier/go-toml/v2" +) + +// MasterConfig is the configuration for the mcp-master daemon. +type MasterConfig struct { + Server ServerConfig `toml:"server"` + Database DatabaseConfig `toml:"database"` + MCIAS MCIASConfig `toml:"mcias"` + Edge EdgeConfig `toml:"edge"` + Registration RegistrationConfig `toml:"registration"` + Timeouts TimeoutsConfig `toml:"timeouts"` + MCNS MCNSConfig `toml:"mcns"` + Log LogConfig `toml:"log"` + Nodes []MasterNodeConfig `toml:"nodes"` + + // Master holds the master's own MCIAS service token for dialing agents. + Master MasterSettings `toml:"master"` +} + +// MasterSettings holds settings specific to the master's own identity. +type MasterSettings struct { + // ServiceTokenPath is the path to the MCIAS service token file + // used by the master to authenticate to agents. + ServiceTokenPath string `toml:"service_token_path"` + + // CACert is the path to the CA certificate for verifying agent TLS. + CACert string `toml:"ca_cert"` +} + +// EdgeConfig holds settings for edge route management. +type EdgeConfig struct { + // AllowedDomains is the list of domains that public hostnames + // must fall under. Validation uses proper domain label matching. + AllowedDomains []string `toml:"allowed_domains"` +} + +// RegistrationConfig holds agent registration settings. +type RegistrationConfig struct { + // AllowedAgents is the list of MCIAS service identities permitted + // to register with the master (e.g., "agent-rift", "agent-svc"). + AllowedAgents []string `toml:"allowed_agents"` + + // MaxNodes is the maximum number of registered nodes. + MaxNodes int `toml:"max_nodes"` +} + +// TimeoutsConfig holds timeout durations for master operations. +type TimeoutsConfig struct { + Deploy Duration `toml:"deploy"` + EdgeRoute Duration `toml:"edge_route"` + HealthCheck Duration `toml:"health_check"` + Undeploy Duration `toml:"undeploy"` + Snapshot Duration `toml:"snapshot"` +} + +// MasterNodeConfig is a bootstrap node entry in the master config. +type MasterNodeConfig struct { + Name string `toml:"name"` + Address string `toml:"address"` + Role string `toml:"role"` // "worker", "edge", or "master" +} + +// LoadMasterConfig reads and validates a master configuration file. +func LoadMasterConfig(path string) (*MasterConfig, 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 MasterConfig + if err := toml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config %q: %w", path, err) + } + + applyMasterDefaults(&cfg) + applyMasterEnvOverrides(&cfg) + + if err := validateMasterConfig(&cfg); err != nil { + return nil, fmt.Errorf("validate config: %w", err) + } + + return &cfg, nil +} + +func applyMasterDefaults(cfg *MasterConfig) { + if cfg.Log.Level == "" { + cfg.Log.Level = "info" + } + if cfg.Registration.MaxNodes == 0 { + cfg.Registration.MaxNodes = 16 + } + if cfg.Timeouts.Deploy.Duration == 0 { + cfg.Timeouts.Deploy.Duration = 5 * time.Minute + } + if cfg.Timeouts.EdgeRoute.Duration == 0 { + cfg.Timeouts.EdgeRoute.Duration = 30 * time.Second + } + if cfg.Timeouts.HealthCheck.Duration == 0 { + cfg.Timeouts.HealthCheck.Duration = 5 * time.Second + } + if cfg.Timeouts.Undeploy.Duration == 0 { + cfg.Timeouts.Undeploy.Duration = 2 * time.Minute + } + if cfg.Timeouts.Snapshot.Duration == 0 { + cfg.Timeouts.Snapshot.Duration = 10 * time.Minute + } + if cfg.MCNS.Zone == "" { + cfg.MCNS.Zone = "svc.mcp.metacircular.net" + } + for i := range cfg.Nodes { + if cfg.Nodes[i].Role == "" { + cfg.Nodes[i].Role = "worker" + } + } +} + +func applyMasterEnvOverrides(cfg *MasterConfig) { + if v := os.Getenv("MCP_MASTER_SERVER_GRPC_ADDR"); v != "" { + cfg.Server.GRPCAddr = v + } + if v := os.Getenv("MCP_MASTER_SERVER_TLS_CERT"); v != "" { + cfg.Server.TLSCert = v + } + if v := os.Getenv("MCP_MASTER_SERVER_TLS_KEY"); v != "" { + cfg.Server.TLSKey = v + } + if v := os.Getenv("MCP_MASTER_DATABASE_PATH"); v != "" { + cfg.Database.Path = v + } + if v := os.Getenv("MCP_MASTER_LOG_LEVEL"); v != "" { + cfg.Log.Level = v + } +} + +func validateMasterConfig(cfg *MasterConfig) 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 len(cfg.Nodes) == 0 { + return fmt.Errorf("at least one [[nodes]] entry is required") + } + if cfg.Master.ServiceTokenPath == "" { + return fmt.Errorf("master.service_token_path is required") + } + return nil +}