Pass effective paths through configuration loading

This commit is contained in:
2026-10-17 05:33:03 +00:00
parent 5e58b6944b
commit 0cd833513c
5 changed files with 90 additions and 32 deletions

View File

@@ -359,6 +359,12 @@ Validation: configuration load, target, prompt-origin, inheritance, and inspecti
tests; `GOWORK=off go test ./internal/configuration .`. A behavior-preserving tests; `GOWORK=off go test ./internal/configuration .`. A behavior-preserving
refactor should not rewrite unrelated expectations. 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 ## Stage 4: Install One Prompt Definition Per ID
Scope: prompt storage format, materialization, and fixture-format migration. Scope: prompt storage format, materialization, and fixture-format migration.

View File

@@ -53,7 +53,7 @@ func secretProviderPath(path configkit.Path) bool {
return false return false
} }
parts := strings.Split(strings.TrimPrefix(pointer, "/"), "/") 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 return false
} }
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(parts[len(parts)-1], "~1", "/"), "~0", "~")) 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 return false
} }
} }
func providerExtraParamsPath(parts []string) bool {
return len(parts) >= 4 && parts[1] != "" &&
(parts[0] == "backends" || parts[0] == "profiles") && parts[2] == "extra_params"
}

View File

@@ -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)
}
})
}
}

View File

@@ -119,6 +119,20 @@ type loaded struct {
promptIDs []domain.PromptIdentity 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 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"} 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. // load performs one ConfigKit lifecycle over the selected source tiers.
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) { func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
bases := rootResourceBases()
state := &loaded{ state := &loaded{
backends: make(map[string]map[string]fieldState), backends: make(map[string]map[string]fieldState),
profiles: 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]{ Definition: configkit.Definition[document]{
Secret: secretProviderPath, Secret: secretProviderPath,
Resolve: func(config *document, resolution *configkit.Resolution) error { Resolve: func(config *document, resolution *configkit.Resolution) error {
captureFields(config.Backends, "backends", backendFields, resolution, state.backends) captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends)
captureFields(config.Profiles, "profiles", profileFields, resolution, state.profiles) captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles)
capturePromptMetadata(config.Prompts, resolution, state.promptMetadata) capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata)
resolveBackends(config.Backends, resolution) resolveBackends(config.Backends, bases.backends, resolution)
resolveProfiles(config.Profiles, resolution) resolveProfiles(config.Profiles, bases.profiles, resolution)
resolvePrompts(config.Prompts, resolution) resolvePrompts(config.Prompts, bases.prompts, resolution)
return nil return nil
}, },
Validate: validateEffective, Validate: func(config *document, validation *configkit.Validation) {
validateEffective(config, bases, validation)
},
}, },
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("load configuration: %w", err) return nil, fmt.Errorf("load configuration: %w", err)
} }
state.result = result 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) return nil, fmt.Errorf("load configuration: %w", err)
} }
if err := state.buildExecutionProfiles(); err != nil { if err := state.buildExecutionProfiles(); err != nil {
@@ -176,7 +193,7 @@ type promptInputPresence struct {
description fieldState 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) { for _, id := range sortedKeys(prompts) {
if destination[id] == nil { if destination[id] == nil {
destination[id] = make(map[string][]promptInputPresence, len(prompts[id])) 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]) { for _, version := range sortedKeys(prompts[id]) {
inputs := prompts[id][version].Inputs inputs := prompts[id][version].Inputs
states := make([]promptInputPresence, len(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 { for index := range inputs {
path := base.Index(index) path := base.Index(index)
states[index] = promptInputPresence{ 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 { for id := range entries {
states := make(map[string]fieldState, len(fields)) states := make(map[string]fieldState, len(fields))
for _, field := range fields { for _, field := range fields {
info, exists := resolution.InputInfo(configkit.Keys(collection, id, field)) info, exists := resolution.InputInfo(basePath.Key(id).Key(field))
switch { switch {
case !exists || !info.Explicit: case !exists || !info.Explicit:
states[field] = fieldAbsent states[field] = fieldAbsent
@@ -244,10 +261,10 @@ func normalizeString(value **string, path configkit.Path, resolution *configkit.
return false 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) { for _, id := range sortedKeys(backends) {
backend := backends[id] backend := backends[id]
base := configkit.Keys("backends", id) base := basePath.Key(id)
if backend.Endpoint != nil { if backend.Endpoint != nil {
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint) endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
if err != nil { if err != nil {
@@ -301,10 +318,10 @@ func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution
normalizeString(value, path, resolution, normalized) 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) { for _, id := range sortedKeys(profiles) {
profile := profiles[id] profile := profiles[id]
base := configkit.Keys("profiles", id) base := basePath.Key(id)
if profile.Endpoint != nil && *profile.Endpoint != "" { if profile.Endpoint != nil && *profile.Endpoint != "" {
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint) endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
if err != nil { 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) { for _, id := range sortedKeys(config.Backends) {
backend := config.Backends[id] backend := config.Backends[id]
base := configkit.Keys("backends", id) base := bases.backends.Key(id)
if strings.TrimSpace(id) == "" { if strings.TrimSpace(id) == "" {
validation.Error(base, "backend ID must not be blank") 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) { for _, id := range sortedKeys(config.Profiles) {
profile := config.Profiles[id] profile := config.Profiles[id]
base := configkit.Keys("profiles", id) base := bases.profiles.Key(id)
if strings.TrimSpace(id) == "" { if strings.TrimSpace(id) == "" {
validation.Error(base, "profile ID must not be blank") validation.Error(base, "profile ID must not be blank")
} }

View File

@@ -29,11 +29,11 @@ type promptEntry struct {
messages []*template.Template 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 _, id := range sortedKeys(prompts) {
for _, version := range sortedKeys(prompts[id]) { for _, version := range sortedKeys(prompts[id]) {
definition := prompts[id][version] definition := prompts[id][version]
base := configkit.Keys("prompts", id, version) base := basePath.Key(id).Key(version)
for _, field := range []struct { for _, field := range []struct {
name string name string
value **string value **string
@@ -84,8 +84,8 @@ func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *conf
} }
} }
func (state *loaded) materializePrompts() error { func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
state.prompts = make(map[string]map[string]*promptEntry, len(state.result.Config.Prompts)) state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
state.ambiguous = make(map[string]bool) state.ambiguous = make(map[string]bool)
var metadata *configkit.Metadata var metadata *configkit.Metadata
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) { lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
@@ -95,21 +95,23 @@ func (state *loaded) materializePrompts() error {
} }
return metadata.Lookup(path) 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) == "" { 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 { 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.prompts[id] = make(map[string]*promptEntry, len(versions))
state.ambiguous[id] = len(versions) > 1 state.ambiguous[id] = len(versions) > 1
for _, version := range sortedKeys(versions) { for _, version := range sortedKeys(versions) {
versionBase := promptBase.Key(version)
if strings.TrimSpace(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 { if err != nil {
return err return err
} }
@@ -119,8 +121,7 @@ func (state *loaded) materializePrompts() error {
return nil return nil
} }
func (state *loaded) materializePrompt(id, version string, raw promptConfig, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) { func (state *loaded) materializePrompt(id, version string, raw promptConfig, base configkit.Path, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) (*promptEntry, error) {
base := configkit.Keys("prompts", id, version)
if len(raw.Messages) == 0 { if len(raw.Messages) == 0 {
return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages")) return nil, fmt.Errorf("%s: at least one message is required", base.Key("messages"))
} }