@@ -325,6 +325,11 @@ func loadConfig(cwd string, debug bool) (*Config, error) {
325325 mergeMCPs(cfg, globalCfg, localConfig)
326326 mergeLSPs(cfg, globalCfg, localConfig)
327327
328+ // Validate the final configuration
329+ if err := cfg.Validate(); err != nil {
330+ return cfg, fmt.Errorf("configuration validation failed: %w", err)
331+ }
332+
328333 return cfg, nil
329334}
330335
@@ -503,7 +508,7 @@ func mergeAgents(base, global, local *Config) {
503508 base.Agents[agentID] = newAgent
504509 } else {
505510 baseAgent := base.Agents[agentID]
506−
511+
507512 // Special handling for known agents - only allow model changes
508513 if agentID == AgentCoder || agentID == AgentTask {
509514 if newAgent.Model != "" {
@@ -533,10 +538,10 @@ func mergeAgents(base, global, local *Config) {
533538 } else if baseAgent.Model == "" {
534539 baseAgent.Model = LargeModel // Default fallback
535540 }
536−
541+
537542 // Boolean fields - always update (including false values)
538543 baseAgent.Disabled = newAgent.Disabled
539−
544+
540545 // Slice/Map fields - update if provided (including empty slices/maps)
541546 if newAgent.AllowedTools != nil {
542547 baseAgent.AllowedTools = newAgent.AllowedTools
@@ -552,7 +557,7 @@ func mergeAgents(base, global, local *Config) {
552557 baseAgent.ContextPaths = append(baseAgent.ContextPaths, newAgent.ContextPaths...)
553558 }
554559 }
555−
560+
556561 base.Agents[agentID] = baseAgent
557562 }
558563 }
@@ -666,6 +671,9 @@ func defaultConfigBasedOnEnv() *Config {
666671 ContextPaths: defaultContextPaths,
667672 },
668673 Providers: make(map[provider.InferenceProvider]ProviderConfig),
674+ Agents: make(map[AgentID]Agent),
675+ LSP: make(map[string]LSPConfig),
676+ MCP: make(map[string]MCP),
669677 }
670678
671679 providers := Providers()
@@ -681,9 +689,7 @@ func defaultConfigBasedOnEnv() *Config {
681689 baseURL := p.APIEndpoint
682690 if strings.HasPrefix(baseURL, "$") {
683691 envVar := strings.TrimPrefix(baseURL, "$")
684− if url := os.Getenv(envVar); url != "" {
685− baseURL = url
686− }
692+ baseURL = os.Getenv(envVar)
687693 }
688694 providerConfig.BaseURL = baseURL
689695 for _, model := range p.Models {
@@ -871,3 +877,507 @@ func UpdatePreferredModel(modelType ModelType, model PreferredModel) error {
871877 }
872878 return nil
873879}
880+
881+// ValidationError represents a configuration validation error
882+type ValidationError struct {
883+ Field string
884+ Message string
885+}
886+
887+func (e ValidationError) Error() string {
888+ return fmt.Sprintf("validation error in %s: %s", e.Field, e.Message)
889+}
890+
891+// ValidationErrors represents multiple validation errors
892+type ValidationErrors []ValidationError
893+
894+func (e ValidationErrors) Error() string {
895+ if len(e) == 0 {
896+ return "no validation errors"
897+ }
898+ if len(e) == 1 {
899+ return e[0].Error()
900+ }
901+
902+ var messages []string
903+ for _, err := range e {
904+ messages = append(messages, err.Error())
905+ }
906+ return fmt.Sprintf("multiple validation errors: %s", strings.Join(messages, "; "))
907+}
908+
909+// HasErrors returns true if there are any validation errors
910+func (e ValidationErrors) HasErrors() bool {
911+ return len(e) > 0
912+}
913+
914+// Add appends a new validation error
915+func (e *ValidationErrors) Add(field, message string) {
916+ *e = append(*e, ValidationError{Field: field, Message: message})
917+}
918+
919+// Validate performs comprehensive validation of the configuration
920+func (c *Config) Validate() error {
921+ var errors ValidationErrors
922+
923+ // Validate providers
924+ c.validateProviders(&errors)
925+
926+ // Validate models
927+ c.validateModels(&errors)
928+
929+ // Validate agents
930+ c.validateAgents(&errors)
931+
932+ // Validate options
933+ c.validateOptions(&errors)
934+
935+ // Validate MCP configurations
936+ c.validateMCPs(&errors)
937+
938+ // Validate LSP configurations
939+ c.validateLSPs(&errors)
940+
941+ // Validate cross-references
942+ c.validateCrossReferences(&errors)
943+
944+ // Validate completeness
945+ c.validateCompleteness(&errors)
946+
947+ if errors.HasErrors() {
948+ return errors
949+ }
950+
951+ return nil
952+}
953+
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)
958+ }
959+
960+ knownProviders := provider.KnownProviders()
961+ validTypes := []provider.Type{
962+ provider.TypeOpenAI,
963+ provider.TypeAnthropic,
964+ provider.TypeGemini,
965+ provider.TypeAzure,
966+ provider.TypeBedrock,
967+ provider.TypeVertexAI,
968+ provider.TypeXAI,
969+ }
970+
971+ for providerID, providerConfig := range c.Providers {
972+ fieldPrefix := fmt.Sprintf("providers.%s", providerID)
973+
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")
979+ }
980+ }
981+
982+ // Validate provider type
983+ validType := false
984+ for _, vt := range validTypes {
985+ if providerConfig.ProviderType == vt {
986+ validType = true
987+ break
988+ }
989+ }
990+ if !validType {
991+ errors.Add(fieldPrefix+".provider_type", fmt.Sprintf("invalid provider type: %s", providerConfig.ProviderType))
992+ }
993+
994+ // Validate custom providers
995+ isKnownProvider := false
996+ for _, kp := range knownProviders {
997+ if providerID == kp {
998+ isKnownProvider = true
999+ break
1000+ }
1001+ }
1002+
1003+ if !isKnownProvider {
1004+ // Custom provider validation
1005+ if providerConfig.BaseURL == "" {
1006+ errors.Add(fieldPrefix+".base_url", "BaseURL is required for custom providers")
1007+ }
1008+ if providerConfig.ProviderType != provider.TypeOpenAI {
1009+ errors.Add(fieldPrefix+".provider_type", "custom providers currently only support OpenAI type")
1010+ }
1011+ }
1012+
1013+ // Validate models
1014+ modelIDs := make(map[string]bool)
1015+ for i, model := range providerConfig.Models {
1016+ modelFieldPrefix := fmt.Sprintf("%s.models[%d]", fieldPrefix, i)
1017+
1018+ // Check for duplicate model IDs
1019+ if modelIDs[model.ID] {
1020+ errors.Add(modelFieldPrefix+".id", fmt.Sprintf("duplicate model ID: %s", model.ID))
1021+ }
1022+ modelIDs[model.ID] = true
1023+
1024+ // Validate required model fields
1025+ if model.ID == "" {
1026+ errors.Add(modelFieldPrefix+".id", "model ID is required")
1027+ }
1028+ if model.Name == "" {
1029+ errors.Add(modelFieldPrefix+".name", "model name is required")
1030+ }
1031+ if model.ContextWindow <= 0 {
1032+ errors.Add(modelFieldPrefix+".context_window", "context window must be positive")
1033+ }
1034+ if model.DefaultMaxTokens <= 0 {
1035+ errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens must be positive")
1036+ }
1037+ if model.DefaultMaxTokens > model.ContextWindow {
1038+ errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens cannot exceed context window")
1039+ }
1040+
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")
1044+ }
1045+ if model.CostPer1MOut < 0 {
1046+ errors.Add(modelFieldPrefix+".cost_per_1m_out", "cost per 1M output tokens cannot be negative")
1047+ }
1048+ if model.CostPer1MInCached < 0 {
1049+ errors.Add(modelFieldPrefix+".cost_per_1m_in_cached", "cached cost per 1M input tokens cannot be negative")
1050+ }
1051+ if model.CostPer1MOutCached < 0 {
1052+ errors.Add(modelFieldPrefix+".cost_per_1m_out_cached", "cached cost per 1M output tokens cannot be negative")
1053+ }
1054+ }
1055+
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))
1060+ }
1061+ }
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))
1065+ }
1066+ }
1067+
1068+ // Validate provider-specific requirements
1069+ c.validateProviderSpecific(providerID, providerConfig, errors)
1070+ }
1071+}
1072+
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)
1076+
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")
1082+ } else {
1083+ if providerConfig.ExtraParams["project"] == "" {
1084+ errors.Add(fieldPrefix+".extra_params.project", "VertexAI requires project parameter")
1085+ }
1086+ if providerConfig.ExtraParams["location"] == "" {
1087+ errors.Add(fieldPrefix+".extra_params.location", "VertexAI requires location parameter")
1088+ }
1089+ }
1090+ }
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")
1095+ }
1096+ // Check for AWS credentials in environment
1097+ if !hasAWSCredentials() {
1098+ errors.Add(fieldPrefix, "Bedrock requires AWS credentials in environment")
1099+ }
1100+ }
1101+ }
1102+}
1103+
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")
1110+ }
1111+ if c.Models.Large.Provider == "" {
1112+ errors.Add("models.large.provider", "large model provider is required when model ID is set")
1113+ }
1114+
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")
1119+ }
1120+
1121+ // Check if model exists in provider
1122+ modelExists := false
1123+ for _, model := range providerConfig.Models {
1124+ if model.ID == c.Models.Large.ModelID {
1125+ modelExists = true
1126+ break
1127+ }
1128+ }
1129+ if !modelExists {
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))
1131+ }
1132+ } else {
1133+ errors.Add("models.large.provider", fmt.Sprintf("large model provider '%s' not found", c.Models.Large.Provider))
1134+ }
1135+ }
1136+
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")
1141+ }
1142+ if c.Models.Small.Provider == "" {
1143+ errors.Add("models.small.provider", "small model provider is required when model ID is set")
1144+ }
1145+
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")
1150+ }
1151+
1152+ // Check if model exists in provider
1153+ modelExists := false
1154+ for _, model := range providerConfig.Models {
1155+ if model.ID == c.Models.Small.ModelID {
1156+ modelExists = true
1157+ break
1158+ }
1159+ }
1160+ if !modelExists {
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))
1162+ }
1163+ } else {
1164+ errors.Add("models.small.provider", fmt.Sprintf("small model provider '%s' not found", c.Models.Small.Provider))
1165+ }
1166+ }
1167+}
1168+
1169+// validateAgents validates agent configurations
1170+func (c *Config) validateAgents(errors *ValidationErrors) {
1171+ if c.Agents == nil {
1172+ c.Agents = make(map[AgentID]Agent)
1173+ }
1174+
1175+ validTools := []string{
1176+ "bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
1177+ }
1178+
1179+ for agentID, agent := range c.Agents {
1180+ fieldPrefix := fmt.Sprintf("agents.%s", agentID)
1181+
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))
1185+ }
1186+
1187+ // Validate required fields
1188+ if agent.ID == "" {
1189+ errors.Add(fieldPrefix+".id", "agent ID is required")
1190+ }
1191+ if agent.Name == "" {
1192+ errors.Add(fieldPrefix+".name", "agent name is required")
1193+ }
1194+
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))
1198+ }
1199+
1200+ // Validate allowed tools
1201+ if agent.AllowedTools != nil {
1202+ for i, tool := range agent.AllowedTools {
1203+ validTool := false
1204+ for _, vt := range validTools {
1205+ if tool == vt {
1206+ validTool = true
1207+ break
1208+ }
1209+ }
1210+ if !validTool {
1211+ errors.Add(fmt.Sprintf("%s.allowed_tools[%d]", fieldPrefix, i), fmt.Sprintf("unknown tool: %s", tool))
1212+ }
1213+ }
1214+ }
1215+
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))
1221+ }
1222+ }
1223+ }
1224+
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))
1230+ }
1231+ }
1232+ }
1233+
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")
1238+ }
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")
1242+ }
1243+ }
1244+
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")
1249+ }
1250+ if agent.Description != "An agent that helps with executing coding tasks." {
1251+ errors.Add(fieldPrefix+".description", "coder agent description cannot be changed")
1252+ }
1253+ } else if agentID == AgentTask {
1254+ if agent.Name != "Task" {
1255+ errors.Add(fieldPrefix+".name", "task agent name cannot be changed")
1256+ }
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")
1259+ }
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")
1263+ }
1264+ }
1265+ }
1266+}
1267+
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")
1273+ }
1274+
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")
1279+ }
1280+ if strings.Contains(contextPath, "\x00") {
1281+ errors.Add(fmt.Sprintf("options.context_paths[%d]", i), "context path contains invalid characters")
1282+ }
1283+ }
1284+}
1285+
1286+// validateMCPs validates MCP configurations
1287+func (c *Config) validateMCPs(errors *ValidationErrors) {
1288+ if c.MCP == nil {
1289+ c.MCP = make(map[string]MCP)
1290+ }
1291+
1292+ for mcpName, mcpConfig := range c.MCP {
1293+ fieldPrefix := fmt.Sprintf("mcp.%s", mcpName)
1294+
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))
1298+ }
1299+
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")
1304+ }
1305+ } else if mcpConfig.Type == MCPSse {
1306+ if mcpConfig.URL == "" {
1307+ errors.Add(fieldPrefix+".url", "URL is required for SSE MCP")
1308+ }
1309+ }
1310+ }
1311+}
1312+
1313+// validateLSPs validates LSP configurations
1314+func (c *Config) validateLSPs(errors *ValidationErrors) {
1315+ if c.LSP == nil {
1316+ c.LSP = make(map[string]LSPConfig)
1317+ }
1318+
1319+ for lspName, lspConfig := range c.LSP {
1320+ fieldPrefix := fmt.Sprintf("lsp.%s", lspName)
1321+
1322+ if lspConfig.Command == "" {
1323+ errors.Add(fieldPrefix+".command", "command is required for LSP")
1324+ }
1325+ }
1326+}
1327+
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)
1333+
1334+ var preferredModel PreferredModel
1335+ switch agent.Model {
1336+ case LargeModel:
1337+ preferredModel = c.Models.Large
1338+ case SmallModel:
1339+ preferredModel = c.Models.Small
1340+ }
1341+
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))
1346+ }
1347+ }
1348+ }
1349+ }
1350+}
1351+
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
1359+ break
1360+ }
1361+ }
1362+ if !hasValidProvider {
1363+ errors.Add("providers", "at least one non-disabled provider is required")
1364+ }
1365+
1366+ // Check that default agents exist
1367+ if _, exists := c.Agents[AgentCoder]; !exists {
1368+ errors.Add("agents", "coder agent is required")
1369+ }
1370+ if _, exists := c.Agents[AgentTask]; !exists {
1371+ errors.Add("agents", "task agent is required")
1372+ }
1373+
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")
1378+ }
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")
1381+ }
1382+ }
1383+}