@@ -325,6 +325,11 @@ func loadConfig(cwd string, debug bool) (*Config, error) {
325 mergeMCPs(cfg, globalCfg, localConfig)
325 mergeMCPs(cfg, globalCfg, localConfig)
326 mergeLSPs(cfg, globalCfg, localConfig)
326 mergeLSPs(cfg, globalCfg, localConfig)
328+ // Validate the final configuration
329+ if err := cfg.Validate(); err != nil {
330+ return cfg, fmt.Errorf("configuration validation failed: %w", err)
328 return cfg, nil
333 return cfg, nil
@@ -503,7 +508,7 @@ func mergeAgents(base, global, local *Config) {
503 base.Agents[agentID] = newAgent
508 base.Agents[agentID] = newAgent
505 baseAgent := base.Agents[agentID]
510 baseAgent := base.Agents[agentID]
507 // Special handling for known agents - only allow model changes
512 // Special handling for known agents - only allow model changes
508 if agentID == AgentCoder || agentID == AgentTask {
513 if agentID == AgentCoder || agentID == AgentTask {
509 if newAgent.Model != "" {
514 if newAgent.Model != "" {
@@ -533,10 +538,10 @@ func mergeAgents(base, global, local *Config) {
533 } else if baseAgent.Model == "" {
538 } else if baseAgent.Model == "" {
534 baseAgent.Model = LargeModel // Default fallback
539 baseAgent.Model = LargeModel // Default fallback
537 // Boolean fields - always update (including false values)
542 // Boolean fields - always update (including false values)
538 baseAgent.Disabled = newAgent.Disabled
543 baseAgent.Disabled = newAgent.Disabled
540 // Slice/Map fields - update if provided (including empty slices/maps)
545 // Slice/Map fields - update if provided (including empty slices/maps)
541 if newAgent.AllowedTools != nil {
546 if newAgent.AllowedTools != nil {
542 baseAgent.AllowedTools = newAgent.AllowedTools
547 baseAgent.AllowedTools = newAgent.AllowedTools
@@ -552,7 +557,7 @@ func mergeAgents(base, global, local *Config) {
552 baseAgent.ContextPaths = append(baseAgent.ContextPaths, newAgent.ContextPaths...)
557 baseAgent.ContextPaths = append(baseAgent.ContextPaths, newAgent.ContextPaths...)
556 base.Agents[agentID] = baseAgent
561 base.Agents[agentID] = baseAgent
@@ -666,6 +671,9 @@ func defaultConfigBasedOnEnv() *Config {
666 ContextPaths: defaultContextPaths,
671 ContextPaths: defaultContextPaths,
668 Providers: make(map[provider.InferenceProvider]ProviderConfig),
673 Providers: make(map[provider.InferenceProvider]ProviderConfig),
674+ Agents: make(map[AgentID]Agent),
675+ LSP: make(map[string]LSPConfig),
676+ MCP: make(map[string]MCP),
671 providers := Providers()
679 providers := Providers()
@@ -681,9 +689,7 @@ func defaultConfigBasedOnEnv() *Config {
681 baseURL := p.APIEndpoint
689 baseURL := p.APIEndpoint
682 if strings.HasPrefix(baseURL, "$") {
690 if strings.HasPrefix(baseURL, "$") {
683 envVar := strings.TrimPrefix(baseURL, "$")
691 envVar := strings.TrimPrefix(baseURL, "$")
684− if url := os.Getenv(envVar); url != "" {
692+ baseURL = os.Getenv(envVar)
688 providerConfig.BaseURL = baseURL
694 providerConfig.BaseURL = baseURL
689 for _, model := range p.Models {
695 for _, model := range p.Models {
@@ -871,3 +877,507 @@ func UpdatePreferredModel(modelType ModelType, model PreferredModel) error {
872 return nil
878 return nil
881+// ValidationError represents a configuration validation error
882+type ValidationError struct {
887+func (e ValidationError) Error() string {
888+ return fmt.Sprintf("validation error in %s: %s", e.Field, e.Message)
891+// ValidationErrors represents multiple validation errors
892+type ValidationErrors []ValidationError
894+func (e ValidationErrors) Error() string {
896+ return "no validation errors"
902+ var messages []string
903+ for _, err := range e {
904+ messages = append(messages, err.Error())
906+ return fmt.Sprintf("multiple validation errors: %s", strings.Join(messages, "; "))
909+// HasErrors returns true if there are any validation errors
910+func (e ValidationErrors) HasErrors() bool {
914+// Add appends a new validation error
915+func (e *ValidationErrors) Add(field, message string) {
916+ *e = append(*e, ValidationError{Field: field, Message: message})
919+// Validate performs comprehensive validation of the configuration
920+func (c *Config) Validate() error {
921+ var errors ValidationErrors
923+ // Validate providers
924+ c.validateProviders(&errors)
927+ c.validateModels(&errors)
930+ c.validateAgents(&errors)
933+ c.validateOptions(&errors)
935+ // Validate MCP configurations
936+ c.validateMCPs(&errors)
938+ // Validate LSP configurations
939+ c.validateLSPs(&errors)
941+ // Validate cross-references
942+ c.validateCrossReferences(&errors)
944+ // Validate completeness
945+ c.validateCompleteness(&errors)
947+ if errors.HasErrors() {
954+// validateProviders validates all provider configurations
955+func (c *Config) validateProviders(errors *ValidationErrors) {
956+ if c.Providers == nil {
957+ c.Providers = make(map[provider.InferenceProvider]ProviderConfig)
960+ knownProviders := provider.KnownProviders()
961+ validTypes := []provider.Type{
962+ provider.TypeOpenAI,
963+ provider.TypeAnthropic,
964+ provider.TypeGemini,
966+ provider.TypeBedrock,
967+ provider.TypeVertexAI,
971+ for providerID, providerConfig := range c.Providers {
972+ fieldPrefix := fmt.Sprintf("providers.%s", providerID)
974+ // Validate API key for non-disabled providers
975+ if !providerConfig.Disabled && providerConfig.APIKey == "" {
976+ // Special case for AWS Bedrock and VertexAI which may use other auth methods
977+ if providerID != provider.InferenceProviderBedrock && providerID != provider.InferenceProviderVertexAI {
978+ errors.Add(fieldPrefix+".api_key", "API key is required for non-disabled providers")
982+ // Validate provider type
984+ for _, vt := range validTypes {
985+ if providerConfig.ProviderType == vt {
991+ errors.Add(fieldPrefix+".provider_type", fmt.Sprintf("invalid provider type: %s", providerConfig.ProviderType))
994+ // Validate custom providers
995+ isKnownProvider := false
996+ for _, kp := range knownProviders {
997+ if providerID == kp {
998+ isKnownProvider = true
1003+ if !isKnownProvider {
1004+ // Custom provider validation
1005+ if providerConfig.BaseURL == "" {
1006+ errors.Add(fieldPrefix+".base_url", "BaseURL is required for custom providers")
1008+ if providerConfig.ProviderType != provider.TypeOpenAI {
1009+ errors.Add(fieldPrefix+".provider_type", "custom providers currently only support OpenAI type")
1014+ modelIDs := make(map[string]bool)
1015+ for i, model := range providerConfig.Models {
1016+ modelFieldPrefix := fmt.Sprintf("%s.models[%d]", fieldPrefix, i)
1018+ // Check for duplicate model IDs
1019+ if modelIDs[model.ID] {
1020+ errors.Add(modelFieldPrefix+".id", fmt.Sprintf("duplicate model ID: %s", model.ID))
1022+ modelIDs[model.ID] = true
1024+ // Validate required model fields
1025+ if model.ID == "" {
1026+ errors.Add(modelFieldPrefix+".id", "model ID is required")
1028+ if model.Name == "" {
1029+ errors.Add(modelFieldPrefix+".name", "model name is required")
1031+ if model.ContextWindow <= 0 {
1032+ errors.Add(modelFieldPrefix+".context_window", "context window must be positive")
1034+ if model.DefaultMaxTokens <= 0 {
1035+ errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens must be positive")
1037+ if model.DefaultMaxTokens > model.ContextWindow {
1038+ errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens cannot exceed context window")
1041+ // Validate cost fields
1042+ if model.CostPer1MIn < 0 {
1043+ errors.Add(modelFieldPrefix+".cost_per_1m_in", "cost per 1M input tokens cannot be negative")
1045+ if model.CostPer1MOut < 0 {
1046+ errors.Add(modelFieldPrefix+".cost_per_1m_out", "cost per 1M output tokens cannot be negative")
1048+ if model.CostPer1MInCached < 0 {
1049+ errors.Add(modelFieldPrefix+".cost_per_1m_in_cached", "cached cost per 1M input tokens cannot be negative")
1051+ if model.CostPer1MOutCached < 0 {
1052+ errors.Add(modelFieldPrefix+".cost_per_1m_out_cached", "cached cost per 1M output tokens cannot be negative")
1056+ // Validate default model references
1057+ if providerConfig.DefaultLargeModel != "" {
1058+ if !modelIDs[providerConfig.DefaultLargeModel] {
1059+ errors.Add(fieldPrefix+".default_large_model", fmt.Sprintf("default large model '%s' not found in provider models", providerConfig.DefaultLargeModel))
1062+ if providerConfig.DefaultSmallModel != "" {
1063+ if !modelIDs[providerConfig.DefaultSmallModel] {
1064+ errors.Add(fieldPrefix+".default_small_model", fmt.Sprintf("default small model '%s' not found in provider models", providerConfig.DefaultSmallModel))
1068+ // Validate provider-specific requirements
1069+ c.validateProviderSpecific(providerID, providerConfig, errors)
1073+// validateProviderSpecific validates provider-specific requirements
1074+func (c *Config) validateProviderSpecific(providerID provider.InferenceProvider, providerConfig ProviderConfig, errors *ValidationErrors) {
1075+ fieldPrefix := fmt.Sprintf("providers.%s", providerID)
1077+ switch providerID {
1078+ case provider.InferenceProviderVertexAI:
1079+ if !providerConfig.Disabled {
1080+ if providerConfig.ExtraParams == nil {
1081+ errors.Add(fieldPrefix+".extra_params", "VertexAI requires extra_params configuration")
1083+ if providerConfig.ExtraParams["project"] == "" {
1084+ errors.Add(fieldPrefix+".extra_params.project", "VertexAI requires project parameter")
1086+ if providerConfig.ExtraParams["location"] == "" {
1087+ errors.Add(fieldPrefix+".extra_params.location", "VertexAI requires location parameter")
1091+ case provider.InferenceProviderBedrock:
1092+ if !providerConfig.Disabled {
1093+ if providerConfig.ExtraParams == nil || providerConfig.ExtraParams["region"] == "" {
1094+ errors.Add(fieldPrefix+".extra_params.region", "Bedrock requires region parameter")
1096+ // Check for AWS credentials in environment
1097+ if !hasAWSCredentials() {
1098+ errors.Add(fieldPrefix, "Bedrock requires AWS credentials in environment")
1104+// validateModels validates preferred model configurations
1105+func (c *Config) validateModels(errors *ValidationErrors) {
1106+ // Validate large model
1107+ if c.Models.Large.ModelID != "" || c.Models.Large.Provider != "" {
1108+ if c.Models.Large.ModelID == "" {
1109+ errors.Add("models.large.model_id", "large model ID is required when provider is set")
1111+ if c.Models.Large.Provider == "" {
1112+ errors.Add("models.large.provider", "large model provider is required when model ID is set")
1115+ // Check if provider exists and is not disabled
1116+ if providerConfig, exists := c.Providers[c.Models.Large.Provider]; exists {
1117+ if providerConfig.Disabled {
1118+ errors.Add("models.large.provider", "large model provider is disabled")
1121+ // Check if model exists in provider
1122+ modelExists := false
1123+ for _, model := range providerConfig.Models {
1124+ if model.ID == c.Models.Large.ModelID {
1130+ errors.Add("models.large.model_id", fmt.Sprintf("large model '%s' not found in provider '%s'", c.Models.Large.ModelID, c.Models.Large.Provider))
1133+ errors.Add("models.large.provider", fmt.Sprintf("large model provider '%s' not found", c.Models.Large.Provider))
1137+ // Validate small model
1138+ if c.Models.Small.ModelID != "" || c.Models.Small.Provider != "" {
1139+ if c.Models.Small.ModelID == "" {
1140+ errors.Add("models.small.model_id", "small model ID is required when provider is set")
1142+ if c.Models.Small.Provider == "" {
1143+ errors.Add("models.small.provider", "small model provider is required when model ID is set")
1146+ // Check if provider exists and is not disabled
1147+ if providerConfig, exists := c.Providers[c.Models.Small.Provider]; exists {
1148+ if providerConfig.Disabled {
1149+ errors.Add("models.small.provider", "small model provider is disabled")
1152+ // Check if model exists in provider
1153+ modelExists := false
1154+ for _, model := range providerConfig.Models {
1155+ if model.ID == c.Models.Small.ModelID {
1161+ errors.Add("models.small.model_id", fmt.Sprintf("small model '%s' not found in provider '%s'", c.Models.Small.ModelID, c.Models.Small.Provider))
1164+ errors.Add("models.small.provider", fmt.Sprintf("small model provider '%s' not found", c.Models.Small.Provider))
1169+// validateAgents validates agent configurations
1170+func (c *Config) validateAgents(errors *ValidationErrors) {
1171+ if c.Agents == nil {
1172+ c.Agents = make(map[AgentID]Agent)
1175+ validTools := []string{
1176+ "bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
1179+ for agentID, agent := range c.Agents {
1180+ fieldPrefix := fmt.Sprintf("agents.%s", agentID)
1182+ // Validate agent ID consistency
1183+ if agent.ID != agentID {
1184+ errors.Add(fieldPrefix+".id", fmt.Sprintf("agent ID mismatch: expected '%s', got '%s'", agentID, agent.ID))
1187+ // Validate required fields
1188+ if agent.ID == "" {
1189+ errors.Add(fieldPrefix+".id", "agent ID is required")
1191+ if agent.Name == "" {
1192+ errors.Add(fieldPrefix+".name", "agent name is required")
1195+ // Validate model type
1196+ if agent.Model != LargeModel && agent.Model != SmallModel {
1197+ errors.Add(fieldPrefix+".model", fmt.Sprintf("invalid model type: %s (must be 'large' or 'small')", agent.Model))
1200+ // Validate allowed tools
1201+ if agent.AllowedTools != nil {
1202+ for i, tool := range agent.AllowedTools {
1204+ for _, vt := range validTools {
1211+ errors.Add(fmt.Sprintf("%s.allowed_tools[%d]", fieldPrefix, i), fmt.Sprintf("unknown tool: %s", tool))
1216+ // Validate MCP references
1217+ if agent.AllowedMCP != nil {
1218+ for mcpName := range agent.AllowedMCP {
1219+ if _, exists := c.MCP[mcpName]; !exists {
1220+ errors.Add(fieldPrefix+".allowed_mcp", fmt.Sprintf("referenced MCP '%s' not found", mcpName))
1225+ // Validate LSP references
1226+ if agent.AllowedLSP != nil {
1227+ for _, lspName := range agent.AllowedLSP {
1228+ if _, exists := c.LSP[lspName]; !exists {
1229+ errors.Add(fieldPrefix+".allowed_lsp", fmt.Sprintf("referenced LSP '%s' not found", lspName))
1234+ // Validate context paths (basic path validation)
1235+ for i, contextPath := range agent.ContextPaths {
1236+ if contextPath == "" {
1237+ errors.Add(fmt.Sprintf("%s.context_paths[%d]", fieldPrefix, i), "context path cannot be empty")
1239+ // Check for invalid characters in path
1240+ if strings.Contains(contextPath, "\x00") {
1241+ errors.Add(fmt.Sprintf("%s.context_paths[%d]", fieldPrefix, i), "context path contains invalid characters")
1245+ // Validate known agents maintain their core properties
1246+ if agentID == AgentCoder {
1247+ if agent.Name != "Coder" {
1248+ errors.Add(fieldPrefix+".name", "coder agent name cannot be changed")
1250+ if agent.Description != "An agent that helps with executing coding tasks." {
1251+ errors.Add(fieldPrefix+".description", "coder agent description cannot be changed")
1253+ } else if agentID == AgentTask {
1254+ if agent.Name != "Task" {
1255+ errors.Add(fieldPrefix+".name", "task agent name cannot be changed")
1257+ if agent.Description != "An agent that helps with searching for context and finding implementation details." {
1258+ errors.Add(fieldPrefix+".description", "task agent description cannot be changed")
1260+ expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1261+ if agent.AllowedTools != nil && !slices.Equal(agent.AllowedTools, expectedTools) {
1262+ errors.Add(fieldPrefix+".allowed_tools", "task agent allowed tools cannot be changed")
1268+// validateOptions validates configuration options
1269+func (c *Config) validateOptions(errors *ValidationErrors) {
1270+ // Validate data directory
1271+ if c.Options.DataDirectory == "" {
1272+ errors.Add("options.data_directory", "data directory is required")
1275+ // Validate context paths
1276+ for i, contextPath := range c.Options.ContextPaths {
1277+ if contextPath == "" {
1278+ errors.Add(fmt.Sprintf("options.context_paths[%d]", i), "context path cannot be empty")
1280+ if strings.Contains(contextPath, "\x00") {
1281+ errors.Add(fmt.Sprintf("options.context_paths[%d]", i), "context path contains invalid characters")
1286+// validateMCPs validates MCP configurations
1287+func (c *Config) validateMCPs(errors *ValidationErrors) {
1289+ c.MCP = make(map[string]MCP)
1292+ for mcpName, mcpConfig := range c.MCP {
1293+ fieldPrefix := fmt.Sprintf("mcp.%s", mcpName)
1295+ // Validate MCP type
1296+ if mcpConfig.Type != MCPStdio && mcpConfig.Type != MCPSse {
1297+ errors.Add(fieldPrefix+".type", fmt.Sprintf("invalid MCP type: %s (must be 'stdio' or 'sse')", mcpConfig.Type))
1300+ // Validate based on type
1301+ if mcpConfig.Type == MCPStdio {
1302+ if mcpConfig.Command == "" {
1303+ errors.Add(fieldPrefix+".command", "command is required for stdio MCP")
1305+ } else if mcpConfig.Type == MCPSse {
1306+ if mcpConfig.URL == "" {
1307+ errors.Add(fieldPrefix+".url", "URL is required for SSE MCP")
1313+// validateLSPs validates LSP configurations
1314+func (c *Config) validateLSPs(errors *ValidationErrors) {
1316+ c.LSP = make(map[string]LSPConfig)
1319+ for lspName, lspConfig := range c.LSP {
1320+ fieldPrefix := fmt.Sprintf("lsp.%s", lspName)
1322+ if lspConfig.Command == "" {
1323+ errors.Add(fieldPrefix+".command", "command is required for LSP")
1328+// validateCrossReferences validates cross-references between different config sections
1329+func (c *Config) validateCrossReferences(errors *ValidationErrors) {
1330+ // Validate that agents can use their assigned model types
1331+ for agentID, agent := range c.Agents {
1332+ fieldPrefix := fmt.Sprintf("agents.%s", agentID)
1334+ var preferredModel PreferredModel
1335+ switch agent.Model {
1337+ preferredModel = c.Models.Large
1339+ preferredModel = c.Models.Small
1342+ if preferredModel.Provider != "" {
1343+ if providerConfig, exists := c.Providers[preferredModel.Provider]; exists {
1344+ if providerConfig.Disabled {
1345+ errors.Add(fieldPrefix+".model", fmt.Sprintf("agent cannot use model type '%s' because provider '%s' is disabled", agent.Model, preferredModel.Provider))
1352+// validateCompleteness validates that the configuration is complete and usable
1353+func (c *Config) validateCompleteness(errors *ValidationErrors) {
1354+ // Check for at least one valid, non-disabled provider
1355+ hasValidProvider := false
1356+ for _, providerConfig := range c.Providers {
1357+ if !providerConfig.Disabled {
1358+ hasValidProvider = true
1362+ if !hasValidProvider {
1363+ errors.Add("providers", "at least one non-disabled provider is required")
1366+ // Check that default agents exist
1367+ if _, exists := c.Agents[AgentCoder]; !exists {
1368+ errors.Add("agents", "coder agent is required")
1370+ if _, exists := c.Agents[AgentTask]; !exists {
1371+ errors.Add("agents", "task agent is required")
1374+ // Check that preferred models are set if providers exist
1375+ if hasValidProvider {
1376+ if c.Models.Large.ModelID == "" || c.Models.Large.Provider == "" {
1377+ errors.Add("models.large", "large preferred model must be configured when providers are available")
1379+ if c.Models.Small.ModelID == "" || c.Models.Small.Provider == "" {
1380+ errors.Add("models.small", "small preferred model must be configured when providers are available")