Retain inherited configuration inspection provenance

This commit is contained in:
2026-10-03 18:03:40 +00:00
parent f4779fede9
commit 1d1434b118
3 changed files with 186 additions and 2 deletions

View File

@@ -15,7 +15,7 @@ runs anything.
| Boundary | Inputs | Outputs | Does not own |
| --- | --- | --- | --- |
| Loading | Selected file path, profile, overrides, and invocation directory | **LoadedConfig** with ConfigKit inspection metadata and a populated **Config** | Choosing the file path or reporting a command result. |
| Loading | Selected file path, overlay, overrides, and invocation directory | **LoadedConfig** with ConfigKit inspection metadata and a populated **Config** | Choosing the file path or reporting a command result. |
| Validation | **Config** | Structural configuration errors with pipeline, lane, or binding context | Module availability, capabilities, or construction. |
| Resolution | Valid **Config**, selected pipeline and lanes, runtime reference changes, LLM override, and module catalog | **EffectiveConfig** with a **ResolvedPipeline** | Materializing reference bytes, preparing modules, execution, or filesystem state. |
| Inspection | **LoadedConfig** and an optional JSON Pointer | Redacted effective YAML or explanation text | Pipeline selection, module preparation, or provider work. |
@@ -34,7 +34,15 @@ inheritance before typed decoding. The document definition supplies typed
defaults and structural validation; Notarius's recorded resolution changes
normalize configured paths after decode. Path normalization follows the source
of the surviving value, including a value copied through inheritance, rather
than the inheritance directive's location. Only
than the inheritance directive's location. Inherited input remains explicit
for presence-sensitive behavior, including zero, false, and empty values;
existing field validation still governs whether the resulting values are valid.
ConfigKit's inspection history retains inheritance transfers alongside later
normalization, and effective configuration renders the normalized values used
by runtime conversion. The private typed document consumes the directive, so
no upstream inheritance type or unresolved directive reaches runtime pipeline
or module contracts. See [Configuration](../config.md#sibling-inheritance)
for the operator syntax and field rules. Only
`NOTARIUS_CONFIG` participates in discovery; retired field-level environment
variables do not participate in loading. Version 5 decoding rejects scalar
module bindings and scalar external references. ConfigKit retains redacted,

View File

@@ -286,6 +286,8 @@ Inspection rendering is checked separately in Stage 5.
## Stage 5 — Preserve Inspection, Derivation, And Ownership
> Completion: complete.
Goal: prove inherited configuration remains correctly inspectable after
Notarius's recorded normalization and runtime conversion.

View File

@@ -1,9 +1,11 @@
package config
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
@@ -433,3 +435,175 @@ configkit:
t.Errorf("overlaid inherited step reference path = %q, want %q", got, want)
}
}
func TestInheritedInspectionRetainsOriginsRedactionAndOwnership(t *testing.T) {
t.Parallel()
root := t.TempDir()
parentDirectory := filepath.Join(root, "parents")
childDirectory := filepath.Join(root, "children")
overlayDirectory := filepath.Join(root, "overlays")
for _, directory := range []string{parentDirectory, childDirectory, overlayDirectory} {
if err := os.MkdirAll(directory, 0o700); err != nil {
t.Fatal(err)
}
}
parentFile := filepath.Join(parentDirectory, "pipelines.yml")
if err := os.WriteFile(parentFile, []byte(`pipelines:
parent:
input: {module: input}
structured_output_repair_attempts: 2
references:
source: {path: ./shared.txt}
artifacts:
summary:
extract:
module: extract
options:
nested: {value: original}
credentials: {api_key: fake-parent-secret}
`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(parentDirectory, "shared.txt"), []byte("source\n"), 0o600); err != nil {
t.Fatal(err)
}
childFile := filepath.Join(childDirectory, "pipelines.yml")
if err := os.WriteFile(childFile, []byte(`pipelines:
child:
extends: parent
structured_output_repair_attempts: 0
`), 0o600); err != nil {
t.Fatal(err)
}
main := filepath.Join(root, "notarius.yml")
if err := os.WriteFile(main, []byte(`version: 5
configkit:
imports:
- path: parents/pipelines.yml
- path: children/pipelines.yml
`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(overlayDirectory, "development.yml"), []byte(`pipelines:
parent:
artifacts:
summary:
extract:
options:
credentials: {api_key: fake-overlay-secret}
`), 0o600); err != nil {
t.Fatal(err)
}
loaded, err := Load(main, LoadOptions{Overlay: "development"})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
child := loaded.Config.Pipelines["child"]
wantPath := filepath.Join(parentDirectory, "shared.txt")
if got := child.References["source"].Path; got != wantPath {
t.Fatalf("runtime inherited path = %q, want %q", got, wantPath)
}
if child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 {
t.Fatalf("runtime inherited repair setting = %#v", child.StructuredOutputRepairAttempts)
}
var effective bytes.Buffer
if err := loaded.WriteEffective(&effective); err != nil {
t.Fatalf("WriteEffective() error = %v", err)
}
if !strings.Contains(effective.String(), wantPath) || strings.Contains(effective.String(), "extends:") ||
!strings.Contains(effective.String(), "structured_output_repair_attempts: 0") {
t.Fatalf("effective configuration is inconsistent with runtime conversion: %s", effective.String())
}
for _, secret := range []string{"fake-parent-secret", "fake-overlay-secret"} {
if strings.Contains(effective.String(), secret) {
t.Fatalf("effective configuration exposed %q", secret)
}
}
for _, test := range []struct {
name string
path configkit.Path
}{
{name: "repair setting", path: configkit.Keys("pipelines", "child", "structured_output_repair_attempts")},
{name: "normalized reference", path: configkit.Keys("pipelines", "child", "references", "source", "path")},
} {
t.Run(test.name, func(t *testing.T) {
explanation, ok := loaded.result.Explain(test.path)
if !ok || len(explanation.History) == 0 {
t.Fatalf("inherited explanation = %#v, found=%v", explanation, ok)
}
inheritanceFromParent := false
for _, change := range explanation.History {
if len(change.Origin.Inheritance) == 0 {
continue
}
for _, location := range change.Origin.Locations {
for _, source := range loaded.result.Sources() {
if source.ID == location.SourceID && source.Path == parentFile {
inheritanceFromParent = true
}
}
}
}
if !inheritanceFromParent {
t.Fatalf("inheritance history lacks parent source evidence: %#v", explanation.History)
}
var rendered bytes.Buffer
if err := loaded.WriteExplanation(&rendered, test.path.String()); err != nil {
t.Fatalf("WriteExplanation() error = %v", err)
}
if !strings.Contains(rendered.String(), filepath.ToSlash(filepath.Join("parents", "pipelines.yml"))) {
t.Fatalf("explanation lacks parent source evidence: %s", rendered.String())
}
if test.name == "normalized reference" {
foundNormalization := false
for _, change := range explanation.History {
if change.Origin.Operation == configkit.ChangeNormalization {
foundNormalization = true
}
}
if !foundNormalization || !strings.Contains(rendered.String(), wantPath) {
t.Fatalf("normalized path evidence = %#v; rendered %s", explanation.History, rendered.String())
}
}
})
}
secretPath := configkit.Keys("pipelines", "child", "artifacts", "summary", "extract", "options", "credentials", "api_key")
secretExplanation, ok := loaded.result.Explain(secretPath)
if !ok || !secretExplanation.Effective.Redacted {
t.Fatalf("inherited secret explanation = %#v, found=%v", secretExplanation, ok)
}
redactedHistory := false
for _, change := range secretExplanation.History {
redactedHistory = redactedHistory || change.Before.Redacted || change.After.Redacted
}
if !redactedHistory {
t.Fatalf("secret history was not redacted: %#v", secretExplanation.History)
}
var secretOutput bytes.Buffer
if err := loaded.WriteExplanation(&secretOutput, secretPath.String()); err != nil {
t.Fatalf("WriteExplanation(secret) error = %v", err)
}
if strings.Contains(secretOutput.String(), "fake-parent-secret") || strings.Contains(secretOutput.String(), "fake-overlay-secret") ||
!strings.Contains(secretOutput.String(), "[REDACTED]") {
t.Fatalf("secret explanation did not redact values and history: %s", secretOutput.String())
}
childOptions := loaded.Config.Pipelines["child"].Artifacts["summary"].Extract.Options
childOptions["nested"].(map[string]any)["value"] = "changed"
parentValue := loaded.Config.Pipelines["parent"].Artifacts["summary"].Extract.Options["nested"].(map[string]any)["value"]
if parentValue != "original" {
t.Fatalf("child runtime mutation changed parent value: %#v", parentValue)
}
var frozen bytes.Buffer
if err := loaded.WriteEffective(&frozen); err != nil {
t.Fatalf("WriteEffective() after runtime mutation error = %v", err)
}
if !strings.Contains(frozen.String(), "original") || strings.Contains(frozen.String(), "changed") {
t.Fatalf("runtime mutation changed frozen inspection: %s", frozen.String())
}
}