package agentconfig import ( "fmt" "regexp" "strings" ) var stableIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*$`) var validCapabilities = map[string]struct{}{ "approval_bypass": {}, "cancel": {}, "resume": {}, "run": {}, "status": {}, "unattended": {}, "writable_root_confinement": {}, } var validModes = map[string]struct{}{ "": {}, "antigravity-print": {}, "codex-app-server": {}, "codex-exec": {}, "opencode-sse": {}, "persistent-lazy": {}, } // Validate checks stable IDs, cross references, probes, capabilities, and the // repository catalog's secret-free boundary. func (c Catalog) Validate() error { if c.Version != SchemaVersion { return fmt.Errorf("agentconfig: unsupported catalog version %q", c.Version) } if len(c.Providers) == 0 || len(c.Models) == 0 || len(c.Profiles) == 0 { return fmt.Errorf("agentconfig: providers, models, and profiles must be non-empty") } providers := make(map[string]Provider, len(c.Providers)) for _, provider := range c.Providers { if err := validateID("provider", provider.ID); err != nil { return err } if _, exists := providers[provider.ID]; exists { return fmt.Errorf("agentconfig: duplicate provider id %q", provider.ID) } if strings.TrimSpace(provider.Command) == "" { return fmt.Errorf("agentconfig: provider %q command is required", provider.ID) } if len(provider.VersionProbe.Args) == 0 { return fmt.Errorf("agentconfig: provider %q version_probe.args is required", provider.ID) } if len(provider.Authentication.Args) == 0 { return fmt.Errorf("agentconfig: provider %q authentication.args is required", provider.ID) } if err := validateTimeout("provider "+provider.ID+" version probe", provider.VersionProbe.TimeoutMS); err != nil { return err } if err := validateTimeout("provider "+provider.ID+" authentication probe", provider.Authentication.TimeoutMS); err != nil { return err } if len(provider.ModelProbe.Args) > 0 { if err := validateTimeout("provider "+provider.ID+" model probe", provider.ModelProbe.TimeoutMS); err != nil { return err } } for field, expression := range map[string]string{ "success_pattern": provider.Authentication.SuccessPattern, "unauthenticated_pattern": provider.Authentication.UnauthenticatedPattern, } { if expression != "" { if _, err := regexp.Compile(expression); err != nil { return fmt.Errorf("agentconfig: provider %q authentication.%s: %w", provider.ID, field, err) } } } if err := validateCapabilities("provider "+provider.ID, provider.Capabilities); err != nil { return err } providers[provider.ID] = provider } models := make(map[string]Model, len(c.Models)) for _, model := range c.Models { if err := validateID("model", model.ID); err != nil { return err } if _, exists := models[model.ID]; exists { return fmt.Errorf("agentconfig: duplicate model id %q", model.ID) } if _, exists := providers[model.Provider]; !exists { return fmt.Errorf("agentconfig: model %q references unknown provider %q", model.ID, model.Provider) } if strings.TrimSpace(model.Target) == "" { return fmt.Errorf("agentconfig: model %q target is required", model.ID) } models[model.ID] = model } profiles := make(map[string]struct{}, len(c.Profiles)) for _, profile := range c.Profiles { if err := validateID("profile", profile.ID); err != nil { return err } if _, exists := profiles[profile.ID]; exists { return fmt.Errorf("agentconfig: duplicate profile id %q", profile.ID) } provider, providerExists := providers[profile.Provider] if !providerExists { return fmt.Errorf("agentconfig: profile %q references unknown provider %q", profile.ID, profile.Provider) } model, modelExists := models[profile.Model] if !modelExists { return fmt.Errorf("agentconfig: profile %q references unknown model %q", profile.ID, profile.Model) } if model.Provider != profile.Provider { return fmt.Errorf( "agentconfig: profile %q provider %q does not own model %q", profile.ID, profile.Provider, profile.Model, ) } if _, exists := validModes[profile.Mode]; !exists { return fmt.Errorf("agentconfig: profile %q has unsupported mode %q", profile.ID, profile.Mode) } if profile.ResponseIdleTimeoutMS < 0 || profile.StartupIdleTimeoutMS < 0 { return fmt.Errorf("agentconfig: profile %q idle timeouts must be non-negative", profile.ID) } if profile.MaxConcurrency < 0 { return fmt.Errorf("agentconfig: profile %q max_concurrency must be non-negative", profile.ID) } if err := validateCapabilities("profile "+profile.ID, profile.Capabilities); err != nil { return err } providerCaps := make(map[string]struct{}, len(provider.Capabilities)) for _, capability := range provider.Capabilities { providerCaps[capability] = struct{}{} } for _, capability := range profile.Capabilities { if _, exists := providerCaps[capability]; !exists { return fmt.Errorf( "agentconfig: profile %q capability %q is not declared by provider %q", profile.ID, capability, profile.Provider, ) } } for _, env := range profile.Env { if secretBearingEnv(env) { return fmt.Errorf("agentconfig: profile %q env %q may contain a tracked secret", profile.ID, envKey(env)) } } profiles[profile.ID] = struct{}{} } return nil } func validateID(kind, id string) error { if !stableIDPattern.MatchString(id) { return fmt.Errorf("agentconfig: invalid %s id %q", kind, id) } return nil } func validateTimeout(label string, timeoutMS int) error { if timeoutMS < 0 || timeoutMS > 120_000 { return fmt.Errorf("agentconfig: %s timeout_ms must be between 0 and 120000", label) } return nil } func validateCapabilities(label string, capabilities []string) error { seen := make(map[string]struct{}, len(capabilities)) for _, capability := range capabilities { if _, exists := validCapabilities[capability]; !exists { return fmt.Errorf("agentconfig: %s has invalid capability %q", label, capability) } if _, exists := seen[capability]; exists { return fmt.Errorf("agentconfig: %s repeats capability %q", label, capability) } seen[capability] = struct{}{} } return nil } func secretBearingEnv(entry string) bool { key := envKey(entry) switch { case key == "AUTHORIZATION", key == "PASSWORD", key == "TOKEN", key == "SECRET", key == "API_KEY", strings.HasSuffix(key, "_PASSWORD"), strings.HasSuffix(key, "_TOKEN"), strings.HasSuffix(key, "_SECRET"), strings.HasSuffix(key, "_API_KEY"), strings.HasSuffix(key, "_ACCESS_KEY"): return true default: return false } } func envKey(entry string) string { key, _, _ := strings.Cut(entry, "=") return strings.ToUpper(strings.TrimSpace(key)) }