From 81f41564a2053adcb22f94d434fae3bb9bb06601 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 26 Aug 2026 16:51:31 +0000 Subject: [PATCH] Harden external catalog validation --- docs/internal/overview.md | 2 +- engine.go | 4 +- internal/catalog/catalog.go | 65 +++++-- internal/catalog/catalog_test.go | 278 ++++++++++++++++++++++++++- internal/profile/eager_repository.go | 17 +- 5 files changed, 334 insertions(+), 32 deletions(-) diff --git a/docs/internal/overview.md b/docs/internal/overview.md index c87edc7..790c25c 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -15,7 +15,7 @@ contributor workflow and validation. | `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) | | `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) | | `internal/backend` | Constructs each engine's immutable registry from maintained definitions and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) | -| `internal/catalog` | Strictly validates imported immutable maintained backend and profile catalog assets before runtime cutover. | [Catalog adapter](../../internal/catalog/catalog.go), [internal sources](sources.md#profiles-and-built-ins) | +| `internal/catalog` | Strictly validates imported immutable maintained backend and profile catalog assets before engine assembly uses them. | [Catalog adapter](../../internal/catalog/catalog.go), [internal sources](sources.md#profiles-and-built-ins) | | `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) | | `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, profiles, execution targets, rendering, generation, and validation, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, and output contracts. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [endpoint invariant](../../internal/domain/endpoint.go) | | `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) | diff --git a/engine.go b/engine.go index 2529e97..3bcf8c9 100644 --- a/engine.go +++ b/engine.go @@ -102,7 +102,7 @@ type Config struct { // prompt source. PromptDir string // ProfileDir is an optional ordinary configured source whose profiles take - // precedence over application fallback and embedded built-in profiles. An + // precedence over application fallback and maintained catalog profiles. An // empty value selects the lower-precedence sources unless a profile-source // option supplies the ordinary source. ProfileDir string @@ -275,7 +275,7 @@ func WithProfileFile(path string) Option { // // Profile lookup checks, in order, profiles supplied by WithProfiles; the // ordinary configured source selected by WithProfileFile, WithProfileFS, or -// Config.ProfileDir; this fallback source; and Promptkit's embedded built-in +// Config.ProfileDir; this fallback source; and Promptkit's maintained catalog // profiles. Each source supplies a complete profile definition; profile fields // are not merged between sources. Only an absent profile ID proceeds to the // next source. A matching read, parse, duplicate, validation, or credential diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index d26e04e..43d42dc 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -10,6 +10,7 @@ import ( "io" "io/fs" "path" + "sort" "strings" "gitea.maximumdirect.net/eric/promptkit/internal/backend" @@ -27,8 +28,9 @@ type Source struct { // Set is the validated maintained backend and raw profile catalog. type Set struct { - Backends []domain.Backend - Profiles profile.Repository + Backends []domain.Backend + Profiles profile.Repository + profileIDs []string } // Load validates and combines immutable catalog sources in source order. @@ -50,7 +52,7 @@ func Load(sources ...Source) (Set, error) { return Set{}, err } if backendIDs[definition.ID] { - return Set{}, fmt.Errorf("catalog %s: duplicate backend ID %q", source.Name, definition.ID) + return Set{}, fmt.Errorf("catalog %s: backend ID duplicates an earlier catalog", source.Name) } backendIDs[definition.ID] = true repository, metadata, err := profile.LoadFSRepository(context.Background(), source.FS, path.Join(source.Root, "profiles")) @@ -60,9 +62,10 @@ func Load(sources ...Source) (Set, error) { if len(metadata) == 0 { return Set{}, fmt.Errorf("catalog %s: profiles must not be empty", source.Name) } + resolvingRepository := profile.NewResolvingRepository(repository) for _, entry := range metadata { if profileIDs[entry.ID] { - return Set{}, fmt.Errorf("catalog %s: duplicate profile ID %q", source.Name, entry.ID) + return Set{}, fmt.Errorf("catalog %s: %s duplicates an earlier profile ID", source.Name, entry.Path) } if containsField(entry.ExplicitFields, "endpoint") || containsField(entry.ExplicitFields, "api_key_env") { return Set{}, fmt.Errorf("catalog %s: %s contains connection metadata", source.Name, entry.Path) @@ -74,14 +77,15 @@ func Load(sources ...Source) (Set, error) { if err := rejectSecretKeys(value.ExtraParams); err != nil { return Set{}, fmt.Errorf("catalog %s: %s: prohibited extra parameter key", source.Name, entry.Path) } - resolved, err := profile.NewResolvingRepository(repository).GetProfile(context.Background(), entry.ID) + resolved, err := resolvingRepository.GetProfile(context.Background(), entry.ID) if err != nil { - return Set{}, fmt.Errorf("catalog %s: %s: %w", source.Name, entry.Path, err) + return Set{}, fmt.Errorf("catalog %s: %s has invalid profile inheritance", source.Name, entry.Path) } if resolved.BackendID != definition.ID { - return Set{}, fmt.Errorf("catalog %s: %s selects backend %q", source.Name, entry.Path, resolved.BackendID) + return Set{}, fmt.Errorf("catalog %s: %s selects a different backend", source.Name, entry.Path) } profileIDs[entry.ID] = true + loaded.profileIDs = append(loaded.profileIDs, entry.ID) } if loaded.Profiles == nil { loaded.Profiles = repository @@ -90,6 +94,7 @@ func Load(sources ...Source) (Set, error) { } loaded.Backends = append(loaded.Backends, definition) } + sort.Strings(loaded.profileIDs) return loaded, nil } @@ -119,10 +124,22 @@ func validateLayout(source Source) error { if err != nil { return err } - if assetPath == source.Root || entry.IsDir() { + if assetPath == source.Root { + if !entry.IsDir() { + return fmt.Errorf("catalog %s: invalid asset path %s", source.Name, assetPath) + } return nil } - if assetPath == manifestPath || strings.HasPrefix(assetPath, profilesRoot+"/") && entry.Type().IsRegular() && strings.HasSuffix(assetPath, ".yml") { + if entry.IsDir() { + if assetPath == profilesRoot || strings.HasPrefix(assetPath, profilesRoot+"/") { + return nil + } + return fmt.Errorf("catalog %s: invalid asset path %s", source.Name, assetPath) + } + if assetPath == manifestPath && entry.Type().IsRegular() { + return nil + } + if strings.HasPrefix(assetPath, profilesRoot+"/") && entry.Type().IsRegular() && strings.HasSuffix(assetPath, ".yml") { return nil } return fmt.Errorf("catalog %s: invalid asset path %s", source.Name, assetPath) @@ -159,22 +176,40 @@ func loadBackend(source Source) (domain.Backend, error) { if *value.ID != source.ExpectedBackendID { return domain.Backend{}, fmt.Errorf("catalog %s: backend ID does not match expected ID", source.Name) } - var extraParams map[string]any - if string(value.ExtraParams) != "null" { - if err := json.Unmarshal(value.ExtraParams, &extraParams); err != nil || extraParams == nil { - return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: invalid extra parameters", source.Name) - } + if strings.TrimSpace(*value.APIKeyEnv) == "" { + return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: api key environment variable must not be blank", source.Name) + } + extraParams, err := decodeExtraParams(value.ExtraParams) + if err != nil { + return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: invalid extra parameters", source.Name) } if err := rejectSecretKeys(extraParams); err != nil { return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: prohibited extra parameter key", source.Name) } normalized, err := backend.NormalizeDefinition(domain.Backend{ID: *value.ID, Endpoint: *value.Endpoint, APIKeyEnv: *value.APIKeyEnv, ExtraParams: extraParams, ConcurrencyLimit: *value.ConcurrencyLimit, QueueCapacity: *value.QueueCapacity, QueueCapacitySet: true}) if err != nil { - return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: %w", source.Name, err) + return domain.Backend{}, fmt.Errorf("catalog %s: backend.json: invalid backend definition", source.Name) } return normalized, nil } +func decodeExtraParams(data []byte) (map[string]any, error) { + if bytes.Equal(bytes.TrimSpace(data), []byte("null")) { + return nil, nil + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var value map[string]any + if err := decoder.Decode(&value); err != nil || value == nil { + return nil, errors.New("extra parameters must be an object or null") + } + var trailing any + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + return nil, errors.New("extra parameters must contain exactly one JSON value") + } + return value, nil +} + func containsField(fields []string, target string) bool { for _, field := range fields { if field == target { diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go index 78ecdc1..292f380 100644 --- a/internal/catalog/catalog_test.go +++ b/internal/catalog/catalog_test.go @@ -4,11 +4,15 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io/fs" "os" "path/filepath" + "reflect" "sort" + "strings" "testing" + "testing/fstest" openrouter "gitea.maximumdirect.net/eric/promptkit-backend-openrouter" rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome" @@ -23,7 +27,11 @@ func TestLoadPublishedCatalogsMatchCompatibilityFixture(t *testing.T) { t.Fatalf("load published catalogs: %v", err) } expected := loadCompatibilityFixture(t) - actual := catalogValue(t, loaded, expected) + expectedIDs := fixtureProfileIDs(t, expected) + if !reflect.DeepEqual(loaded.profileIDs, expectedIDs) { + t.Fatalf("published profile IDs differ from compatibility fixture: got %q, want %q", loaded.profileIDs, expectedIDs) + } + actual := catalogValue(t, loaded) actualJSON, err := json.Marshal(actual) if err != nil { t.Fatalf("encode loaded catalogs: %v", err) @@ -45,7 +53,9 @@ func TestLoadRejectsInvalidSources(t *testing.T) { {Name: "same", ExpectedBackendID: "openrouter", FS: openrouter.FS(), Root: openrouter.Root}, {Name: "same", ExpectedBackendID: "rakestrawhome", FS: rakestrawhome.FS(), Root: rakestrawhome.Root}, }, - "nil filesystem": {{Name: "missing", ExpectedBackendID: "openrouter", Root: openrouter.Root}}, + "nil filesystem": {{Name: "missing", ExpectedBackendID: "openrouter", Root: openrouter.Root}}, + "invalid root": {{Name: "invalid-root", ExpectedBackendID: "openrouter", FS: openrouter.FS(), Root: "."}}, + "blank expected backend": {{Name: "blank-backend", ExpectedBackendID: " ", FS: openrouter.FS(), Root: openrouter.Root}}, } { t.Run(name, func(t *testing.T) { if _, err := Load(sources...); err == nil { @@ -55,6 +65,206 @@ func TestLoadRejectsInvalidSources(t *testing.T) { } } +func TestLoadRejectsInvalidLayouts(t *testing.T) { + tests := map[string]func(fstest.MapFS){ + "unexpected file": func(fsys fstest.MapFS) { + fsys["catalog/notes.txt"] = &fstest.MapFile{Data: []byte("unexpected")} + }, + "unexpected directory": func(fsys fstest.MapFS) { + fsys["catalog/unexpected"] = &fstest.MapFile{Mode: fs.ModeDir} + }, + "nonregular manifest": func(fsys fstest.MapFS) { + fsys["catalog/backend.json"].Mode = fs.ModeSymlink + }, + "wrong profile extension": func(fsys fstest.MapFS) { + fsys["catalog/profiles/extra.yaml"] = &fstest.MapFile{Data: []byte(validProfile("extra", "one"))} + }, + "nonregular profile": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Mode = fs.ModeSymlink + }, + } + for name, mutate := range tests { + t.Run(name, func(t *testing.T) { + fsys := validCatalogFS("one") + mutate(fsys) + if _, err := Load(testSource("one", fsys)); err == nil { + t.Fatal("expected invalid layout error") + } + }) + } +} + +func TestLoadRejectsInvalidManifests(t *testing.T) { + tests := map[string]string{ + "malformed": `{`, + "trailing value": validManifest("one", "TEST_API_KEY", "null") + `{}`, + "missing fields": `{"schema_version":1,"id":"one"}`, + "unsupported version": strings.Replace(validManifest("one", "TEST_API_KEY", "null"), `"schema_version":1`, `"schema_version":2`, 1), + "unknown field": strings.Replace(validManifest("one", "TEST_API_KEY", "null"), `"extra_params":null`, `"extra_params":null,"unknown":true`, 1), + "blank API key env": validManifest("one", " ", "null"), + "invalid API key env": validManifest("one", "LEAK-MARKER", "null"), + "invalid endpoint": strings.Replace(validManifest("one", "TEST_API_KEY", "null"), `https://one.example/v1`, `ftp://leak-marker.invalid/v1`, 1), + "zero concurrency": strings.Replace(validManifest("one", "TEST_API_KEY", "null"), `"concurrency_limit":2`, `"concurrency_limit":0`, 1), + "non-object parameters": validManifest("one", "TEST_API_KEY", `[]`), + "secret parameter": validManifest("one", "TEST_API_KEY", `{"nested":{"token":"leak-marker"}}`), + } + for name, manifest := range tests { + t.Run(name, func(t *testing.T) { + fsys := validCatalogFS("one") + fsys["catalog/backend.json"].Data = []byte(manifest) + _, err := Load(testSource("one", fsys)) + if err == nil { + t.Fatal("expected invalid manifest error") + } + if strings.Contains(strings.ToLower(err.Error()), "leak-marker") { + t.Fatalf("catalog error exposed manifest content: %v", err) + } + }) + } +} + +func TestLoadPreservesManifestJSONNumbers(t *testing.T) { + fsys := validCatalogFS("one") + fsys["catalog/backend.json"].Data = []byte(validManifest( + "one", + "TEST_API_KEY", + `{"large":9007199254740993,"nested":[1.25]}`, + )) + loaded, err := Load(testSource("one", fsys)) + if err != nil { + t.Fatalf("load catalog: %v", err) + } + if got := loaded.Backends[0].ExtraParams["large"]; got != json.Number("9007199254740993") { + t.Fatalf("large JSON integer = %#v, want preserved json.Number", got) + } + nested := loaded.Backends[0].ExtraParams["nested"].([]any) + if nested[0] != json.Number("1.25") { + t.Fatalf("nested JSON number = %#v, want preserved json.Number", nested[0]) + } +} + +func TestLoadRejectsInvalidCatalogProfiles(t *testing.T) { + tests := map[string]func(fstest.MapFS){ + "empty": func(fsys fstest.MapFS) { + delete(fsys, "catalog/profiles/one-profile.yml") + fsys["catalog/profiles"] = &fstest.MapFile{Mode: fs.ModeDir} + }, + "malformed": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte("id: [") + }, + "raw API key": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "one") + "api_key: leak-marker\n") + }, + "endpoint field": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "one") + "endpoint: ''\n") + }, + "API key environment field": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "one") + "api_key_env: ''\n") + }, + "owner mismatch": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "other")) + }, + "missing base": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte("id: one-profile\nbase_profile: leak-marker\n") + }, + "cyclic base": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte("id: one-profile\nbase_profile: second\n") + fsys["catalog/profiles/second.yml"] = &fstest.MapFile{Data: []byte("id: second\nbase_profile: one-profile\n")} + }, + "secret profile parameter": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "one") + "extra_params:\n nested:\n password: leak-marker\n") + }, + "unknown field is redacted": func(fsys fstest.MapFS) { + fsys["catalog/profiles/one-profile.yml"].Data = []byte(validProfile("one-profile", "one") + "leak_marker: leak-marker\n") + }, + } + for name, mutate := range tests { + t.Run(name, func(t *testing.T) { + fsys := validCatalogFS("one") + mutate(fsys) + _, err := Load(testSource("one", fsys)) + if err == nil { + t.Fatal("expected invalid profile error") + } + if strings.Contains(strings.ToLower(err.Error()), "leak-marker") { + t.Fatalf("catalog error exposed profile content: %v", err) + } + }) + } +} + +func TestLoadRejectsCrossCatalogConflicts(t *testing.T) { + t.Run("duplicate backend", func(t *testing.T) { + second := catalogFS("one", map[string]string{ + "catalog/profiles/second.yml": validProfile("second", "one"), + }, "null") + _, err := Load( + Source{Name: "first", ExpectedBackendID: "one", FS: validCatalogFS("one"), Root: "catalog"}, + Source{Name: "second", ExpectedBackendID: "one", FS: second, Root: "catalog"}, + ) + if err == nil { + t.Fatal("expected duplicate backend error") + } + }) + + t.Run("duplicate profile", func(t *testing.T) { + first := catalogFS("one", map[string]string{ + "catalog/profiles/shared.yml": validProfile("shared", "one"), + }, "null") + second := catalogFS("two", map[string]string{ + "catalog/profiles/shared.yml": validProfile("shared", "two"), + }, "null") + _, err := Load(testSource("one", first), testSource("two", second)) + if err == nil { + t.Fatal("expected duplicate profile error") + } + }) + + t.Run("cross-catalog base", func(t *testing.T) { + first := catalogFS("one", map[string]string{ + "catalog/profiles/base.yml": validProfile("base", "one"), + }, "null") + second := catalogFS("two", map[string]string{ + "catalog/profiles/child.yml": "id: child\nbase_profile: base\n", + }, "null") + _, err := Load(testSource("one", first), testSource("two", second)) + if err == nil { + t.Fatal("expected cross-catalog base error") + } + }) +} + +func TestLoadReturnsDefensiveCatalogValues(t *testing.T) { + fsys := catalogFS("one", map[string]string{ + "catalog/profiles/one-profile.yml": validProfile("one-profile", "one") + "extra_params:\n nested:\n value: profile\n", + }, `{"nested":{"value":"backend"}}`) + loaded, err := Load(testSource("one", fsys)) + if err != nil { + t.Fatalf("load catalog: %v", err) + } + loaded.Backends[0].ExtraParams["nested"].(map[string]any)["value"] = "changed" + profileValue, err := loaded.Profiles.GetProfile(context.Background(), "one-profile") + if err != nil { + t.Fatalf("load profile: %v", err) + } + profileValue.ExtraParams["nested"].(map[string]any)["value"] = "changed" + + again, err := Load(testSource("one", fsys)) + if err != nil { + t.Fatalf("reload catalog: %v", err) + } + if got := again.Backends[0].ExtraParams["nested"].(map[string]any)["value"]; got != "backend" { + t.Fatalf("backend mutation escaped returned set: %#v", got) + } + againProfile, err := loaded.Profiles.GetProfile(context.Background(), "one-profile") + if err != nil { + t.Fatalf("reload profile: %v", err) + } + if got := againProfile.ExtraParams["nested"].(map[string]any)["value"]; got != "profile" { + t.Fatalf("profile mutation escaped returned value: %#v", got) + } +} + func loadCompatibilityFixture(t *testing.T) map[string]any { t.Helper() data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "builtin-catalog-v1.json")) @@ -68,7 +278,29 @@ func loadCompatibilityFixture(t *testing.T) map[string]any { return value } -func catalogValue(t *testing.T, loaded Set, fixture map[string]any) map[string]any { +func fixtureProfileIDs(t *testing.T, fixture map[string]any) []string { + t.Helper() + profiles, ok := fixture["profiles"].([]any) + if !ok { + t.Fatal("compatibility fixture profiles are malformed") + } + ids := make([]string, 0, len(profiles)) + for _, entry := range profiles { + profileValue, ok := entry.(map[string]any) + if !ok { + t.Fatal("compatibility fixture profile is malformed") + } + id, ok := profileValue["id"].(string) + if !ok { + t.Fatal("compatibility fixture profile ID is malformed") + } + ids = append(ids, id) + } + sort.Strings(ids) + return ids +} + +func catalogValue(t *testing.T, loaded Set) map[string]any { t.Helper() backends := make([]any, 0, len(loaded.Backends)) for _, backend := range loaded.Backends { @@ -77,10 +309,8 @@ func catalogValue(t *testing.T, loaded Set, fixture map[string]any) map[string]a sort.Slice(backends, func(left, right int) bool { return backends[left].(map[string]any)["id"].(string) < backends[right].(map[string]any)["id"].(string) }) - profiles := fixture["profiles"].([]any) - actualProfiles := make([]any, 0, len(profiles)) - for _, expected := range profiles { - id := expected.(map[string]any)["id"].(string) + actualProfiles := make([]any, 0, len(loaded.profileIDs)) + for _, id := range loaded.profileIDs { profile, err := loaded.Profiles.GetProfile(context.Background(), id) if err != nil { t.Fatalf("load profile %q: %v", id, err) @@ -97,4 +327,38 @@ func interfaceValue(value map[string]any) any { return value } +func testSource(id string, fsys fs.FS) Source { + return Source{Name: id, ExpectedBackendID: id, FS: fsys, Root: "catalog"} +} + +func validCatalogFS(id string) fstest.MapFS { + return catalogFS(id, map[string]string{ + "catalog/profiles/" + id + "-profile.yml": validProfile(id+"-profile", id), + }, "null") +} + +func catalogFS(id string, profiles map[string]string, extraParams string) fstest.MapFS { + fsys := fstest.MapFS{ + "catalog/backend.json": &fstest.MapFile{Data: []byte(validManifest(id, "TEST_API_KEY", extraParams))}, + } + for name, content := range profiles { + fsys[name] = &fstest.MapFile{Data: []byte(content)} + } + return fsys +} + +func validManifest(id, apiKeyEnv, extraParams string) string { + return fmt.Sprintf( + `{"schema_version":1,"id":%q,"endpoint":%q,"api_key_env":%q,"concurrency_limit":2,"queue_capacity":3,"extra_params":%s}`, + id, + "https://"+id+".example/v1", + apiKeyEnv, + extraParams, + ) +} + +func validProfile(id, backendID string) string { + return fmt.Sprintf("id: %s\nbackend: %s\nmodel: test-model\n", id, backendID) +} + var _ fs.FS = openrouter.FS() diff --git a/internal/profile/eager_repository.go b/internal/profile/eager_repository.go index 76d7f1f..acdcc40 100644 --- a/internal/profile/eager_repository.go +++ b/internal/profile/eager_repository.go @@ -14,8 +14,11 @@ import ( // LoadedProfileMetadata identifies one profile accepted by LoadFSRepository. type LoadedProfileMetadata struct { - ID string - Path string + // ID is the normalized profile ID. + ID string + // Path is the safe root-relative source path. + Path string + // ExplicitFields lists the sorted top-level YAML fields present in source. ExplicitFields []string } @@ -42,24 +45,24 @@ func LoadFSRepository(ctx context.Context, fsys fs.FS, root string) (Repository, } fileMetadata, err := readProfileFileMetadata(data) if err != nil { - return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, filecatalog.DisplayPath(root, path), err) + return nil, nil, fmt.Errorf("%w: %s", ErrInvalidYAML, filecatalog.DisplayPath(root, path)) } if fileMetadata.hasRawAPIKey { return nil, nil, fmt.Errorf("%w: %s", ErrRawAPIKeyNotAllowed, filecatalog.DisplayPath(root, path)) } definition, err := decodeProfile(data) if err != nil { - return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, filecatalog.DisplayPath(root, path), err) + return nil, nil, fmt.Errorf("%w: %s", ErrInvalidYAML, filecatalog.DisplayPath(root, path)) } definition.ExtraParams, err = jsonvalue.CopyMap(definition.ExtraParams) if err != nil { - return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidProfile, filecatalog.DisplayPath(root, path), err) + return nil, nil, fmt.Errorf("%w: %s", ErrInvalidProfile, filecatalog.DisplayPath(root, path)) } if err := NormalizeAndValidateDefinition(definition); err != nil { - return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidProfile, filecatalog.DisplayPath(root, path), err) + return nil, nil, fmt.Errorf("%w: %s", ErrInvalidProfile, filecatalog.DisplayPath(root, path)) } if _, exists := repository.profiles[definition.ID]; exists { - return nil, nil, fmt.Errorf("%w: duplicate execution profile id %q", ErrInvalidProfile, definition.ID) + return nil, nil, fmt.Errorf("%w: %s: duplicate profile ID", ErrInvalidProfile, filecatalog.DisplayPath(root, path)) } repository.profiles[definition.ID] = *definition fields := append([]string(nil), fileMetadata.explicitFields...)