From 0cd833513cb7f0be2937a7a72e3e184cda579b12 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 17 Oct 2026 05:33:03 +0000 Subject: [PATCH] Pass effective paths through configuration loading --- docs/roadmap/implementation.md | 6 +++ internal/configuration/inspection.go | 7 ++- internal/configuration/inspection_test.go | 29 ++++++++++++ internal/configuration/load.go | 55 +++++++++++++++-------- internal/configuration/prompts.go | 25 ++++++----- 5 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 internal/configuration/inspection_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 3df3df9..15a5cb0 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -359,6 +359,12 @@ Validation: configuration load, target, prompt-origin, inheritance, and inspecti tests; `GOWORK=off go test ./internal/configuration .`. A behavior-preserving refactor should not rewrite unrelated expectations. +Completion: configuration capture, normalization, effective validation, and +prompt materialization now receive explicit ConfigKit base paths. Existing +resource IDs remain independent from those paths; source-origin reads and the +protected provider-key policy are preserved. Validation passed: +`GOWORK=off go test ./internal/configuration .`. + ## Stage 4: Install One Prompt Definition Per ID Scope: prompt storage format, materialization, and fixture-format migration. diff --git a/internal/configuration/inspection.go b/internal/configuration/inspection.go index b92f0cb..80148cd 100644 --- a/internal/configuration/inspection.go +++ b/internal/configuration/inspection.go @@ -53,7 +53,7 @@ func secretProviderPath(path configkit.Path) bool { return false } parts := strings.Split(strings.TrimPrefix(pointer, "/"), "/") - if len(parts) < 4 || (parts[0] != "backends" && parts[0] != "profiles") || parts[2] != "extra_params" { + if !providerExtraParamsPath(parts) { return false } key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(parts[len(parts)-1], "~1", "/"), "~0", "~")) @@ -64,3 +64,8 @@ func secretProviderPath(path configkit.Path) bool { return false } } + +func providerExtraParamsPath(parts []string) bool { + return len(parts) >= 4 && parts[1] != "" && + (parts[0] == "backends" || parts[0] == "profiles") && parts[2] == "extra_params" +} diff --git a/internal/configuration/inspection_test.go b/internal/configuration/inspection_test.go new file mode 100644 index 0000000..b872a12 --- /dev/null +++ b/internal/configuration/inspection_test.go @@ -0,0 +1,29 @@ +package configuration + +import ( + "testing" + + "gitea.maximumdirect.net/eric/ConfigKit" +) + +func TestSecretProviderPathRecognizesResourceExtraParams(t *testing.T) { + tests := []struct { + name string + path configkit.Path + want bool + }{ + {name: "backend nested secret", path: configkit.Keys("backends", "provider", "extra_params", "nested", "Token"), want: true}, + {name: "profile nested secret", path: configkit.Keys("profiles", "assistant", "extra_params", "authorization"), want: true}, + {name: "unrelated nested mapping", path: configkit.Keys("application", "backends", "provider", "extra_params", "token"), want: false}, + {name: "extra params outside resource map", path: configkit.Keys("backends", "provider", "options", "extra_params", "token"), want: false}, + {name: "missing resource key", path: configkit.Keys("backends", "", "extra_params", "token"), want: false}, + {name: "ordinary provider value", path: configkit.Keys("profiles", "assistant", "extra_params", "provider", "mode"), want: false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := secretProviderPath(test.path); got != test.want { + t.Fatalf("secretProviderPath(%q) = %t, want %t", test.path, got, test.want) + } + }) + } +} diff --git a/internal/configuration/load.go b/internal/configuration/load.go index c4970c3..755afad 100644 --- a/internal/configuration/load.go +++ b/internal/configuration/load.go @@ -119,6 +119,20 @@ type loaded struct { promptIDs []domain.PromptIdentity } +type resourceBases struct { + backends configkit.Path + profiles configkit.Path + prompts configkit.Path +} + +func rootResourceBases() resourceBases { + return resourceBases{ + backends: configkit.Keys("backends"), + profiles: configkit.Keys("profiles"), + prompts: configkit.Keys("prompts"), + } +} + var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"} @@ -131,6 +145,7 @@ func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) // load performs one ConfigKit lifecycle over the selected source tiers. func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) { + bases := rootResourceBases() state := &loaded{ backends: make(map[string]map[string]fieldState), profiles: make(map[string]map[string]fieldState), @@ -141,22 +156,24 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) Definition: configkit.Definition[document]{ Secret: secretProviderPath, Resolve: func(config *document, resolution *configkit.Resolution) error { - captureFields(config.Backends, "backends", backendFields, resolution, state.backends) - captureFields(config.Profiles, "profiles", profileFields, resolution, state.profiles) - capturePromptMetadata(config.Prompts, resolution, state.promptMetadata) - resolveBackends(config.Backends, resolution) - resolveProfiles(config.Profiles, resolution) - resolvePrompts(config.Prompts, resolution) + captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends) + captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles) + capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata) + resolveBackends(config.Backends, bases.backends, resolution) + resolveProfiles(config.Profiles, bases.profiles, resolution) + resolvePrompts(config.Prompts, bases.prompts, resolution) return nil }, - Validate: validateEffective, + Validate: func(config *document, validation *configkit.Validation) { + validateEffective(config, bases, validation) + }, }, }) if err != nil { return nil, fmt.Errorf("load configuration: %w", err) } state.result = result - if err := state.materializePrompts(); err != nil { + if err := state.materializePrompts(result.Config.Prompts, bases.prompts); err != nil { return nil, fmt.Errorf("load configuration: %w", err) } if err := state.buildExecutionProfiles(); err != nil { @@ -176,7 +193,7 @@ type promptInputPresence struct { description fieldState } -func capturePromptMetadata(prompts map[string]map[string]promptConfig, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) { +func capturePromptMetadata(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) { for _, id := range sortedKeys(prompts) { if destination[id] == nil { destination[id] = make(map[string][]promptInputPresence, len(prompts[id])) @@ -184,7 +201,7 @@ func capturePromptMetadata(prompts map[string]map[string]promptConfig, resolutio for _, version := range sortedKeys(prompts[id]) { inputs := prompts[id][version].Inputs states := make([]promptInputPresence, len(inputs)) - base := configkit.Keys("prompts", id, version, "inputs") + base := basePath.Key(id).Key(version).Key("inputs") for index := range inputs { path := base.Index(index) states[index] = promptInputPresence{ @@ -209,11 +226,11 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi } } -func captureFields[T any](entries map[string]T, collection string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { +func captureFields[T any](entries map[string]T, basePath configkit.Path, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { for id := range entries { states := make(map[string]fieldState, len(fields)) for _, field := range fields { - info, exists := resolution.InputInfo(configkit.Keys(collection, id, field)) + info, exists := resolution.InputInfo(basePath.Key(id).Key(field)) switch { case !exists || !info.Explicit: states[field] = fieldAbsent @@ -244,10 +261,10 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit. return false } -func resolveBackends(backends map[string]backendConfig, resolution *configkit.Resolution) { +func resolveBackends(backends map[string]backendConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(backends) { backend := backends[id] - base := configkit.Keys("backends", id) + base := basePath.Key(id) if backend.Endpoint != nil { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint) if err != nil { @@ -301,10 +318,10 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution normalizeString(value, path, resolution, normalized) } -func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Resolution) { +func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(profiles) { profile := profiles[id] - base := configkit.Keys("profiles", id) + base := basePath.Key(id) if profile.Endpoint != nil && *profile.Endpoint != "" { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint) if err != nil { @@ -329,10 +346,10 @@ func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Re } } -func validateEffective(config *document, validation *configkit.Validation) { +func validateEffective(config *document, bases resourceBases, validation *configkit.Validation) { for _, id := range sortedKeys(config.Backends) { backend := config.Backends[id] - base := configkit.Keys("backends", id) + base := bases.backends.Key(id) if strings.TrimSpace(id) == "" { validation.Error(base, "backend ID must not be blank") } @@ -373,7 +390,7 @@ func validateEffective(config *document, validation *configkit.Validation) { } for _, id := range sortedKeys(config.Profiles) { profile := config.Profiles[id] - base := configkit.Keys("profiles", id) + base := bases.profiles.Key(id) if strings.TrimSpace(id) == "" { validation.Error(base, "profile ID must not be blank") } diff --git a/internal/configuration/prompts.go b/internal/configuration/prompts.go index 2565e78..6b08461 100644 --- a/internal/configuration/prompts.go +++ b/internal/configuration/prompts.go @@ -29,11 +29,11 @@ type promptEntry struct { messages []*template.Template } -func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *configkit.Resolution) { +func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(prompts) { for _, version := range sortedKeys(prompts[id]) { definition := prompts[id][version] - base := configkit.Keys("prompts", id, version) + base := basePath.Key(id).Key(version) for _, field := range []struct { name string value **string @@ -84,8 +84,8 @@ func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *conf } } -func (state *loaded) materializePrompts() error { - state.prompts = make(map[string]map[string]*promptEntry, len(state.result.Config.Prompts)) +func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error { + state.prompts = make(map[string]map[string]*promptEntry, len(prompts)) state.ambiguous = make(map[string]bool) var metadata *configkit.Metadata lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) { @@ -95,21 +95,23 @@ func (state *loaded) materializePrompts() error { } return metadata.Lookup(path) } - for _, id := range sortedKeys(state.result.Config.Prompts) { + for _, id := range sortedKeys(prompts) { + promptBase := basePath.Key(id) if strings.TrimSpace(id) == "" { - return fmt.Errorf("%s: prompt ID must not be blank", configkit.Keys("prompts", id)) + return fmt.Errorf("%s: prompt ID must not be blank", promptBase) } - versions := state.result.Config.Prompts[id] + versions := prompts[id] if versions == nil { - return fmt.Errorf("%s: prompt collection must be a mapping", configkit.Keys("prompts", id)) + return fmt.Errorf("%s: prompt collection must be a mapping", promptBase) } state.prompts[id] = make(map[string]*promptEntry, len(versions)) state.ambiguous[id] = len(versions) > 1 for _, version := range sortedKeys(versions) { + versionBase := promptBase.Key(version) if strings.TrimSpace(version) == "" { - return fmt.Errorf("%s: prompt version must not be blank", configkit.Keys("prompts", id, version)) + return fmt.Errorf("%s: prompt version must not be blank", versionBase) } - entry, err := state.materializePrompt(id, version, versions[version], lookupOrigin) + entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin) if err != nil { return err } @@ -119,8 +121,7 @@ func (state *loaded) materializePrompts() error { return nil } -func (state *loaded) materializePrompt(id, version string, raw promptConfig, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) { - base := configkit.Keys("prompts", id, version) +func (state *loaded) materializePrompt(id, version string, raw promptConfig, base configkit.Path, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) { if len(raw.Messages) == 0 { return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages")) }