package agentconfig import ( "fmt" "io" "os" "sort" "gopkg.in/yaml.v3" ) // Load reads exactly one strict YAML document, normalizes deterministic order, // and validates all references before returning. func Load(path string) (Catalog, error) { file, err := os.Open(path) if err != nil { return Catalog{}, fmt.Errorf("agentconfig: open catalog: %w", err) } defer file.Close() decoder := yaml.NewDecoder(file) decoder.KnownFields(true) var catalog Catalog if err := decoder.Decode(&catalog); err != nil { return Catalog{}, fmt.Errorf("agentconfig: decode catalog: %w", err) } var extra any if err := decoder.Decode(&extra); err != io.EOF { if err == nil { return Catalog{}, fmt.Errorf("agentconfig: catalog must contain exactly one YAML document") } return Catalog{}, fmt.Errorf("agentconfig: decode trailing document: %w", err) } return Normalize(catalog) } // Normalize applies non-policy defaults and stable ordering, then validates. func Normalize(catalog Catalog) (Catalog, error) { for i := range catalog.Providers { provider := &catalog.Providers[i] if provider.VersionProbe.TimeoutMS == 0 { provider.VersionProbe.TimeoutMS = DefaultProbeTimeoutMS } if provider.Authentication.TimeoutMS == 0 { provider.Authentication.TimeoutMS = DefaultProbeTimeoutMS } if len(provider.ModelProbe.Args) > 0 && provider.ModelProbe.TimeoutMS == 0 { provider.ModelProbe.TimeoutMS = DefaultProbeTimeoutMS } sort.Strings(provider.Capabilities) } for i := range catalog.Profiles { if catalog.Profiles[i].MaxConcurrency == 0 { catalog.Profiles[i].MaxConcurrency = 1 } sort.Strings(catalog.Profiles[i].Capabilities) } sort.Slice(catalog.Providers, func(i, j int) bool { return catalog.Providers[i].ID < catalog.Providers[j].ID }) sort.Slice(catalog.Models, func(i, j int) bool { return catalog.Models[i].ID < catalog.Models[j].ID }) sort.Slice(catalog.Profiles, func(i, j int) bool { return catalog.Profiles[i].ID < catalog.Profiles[j].ID }) if err := catalog.Validate(); err != nil { return Catalog{}, err } return catalog, nil }