From b64010e7297b9351e1ed92c462ffa81d734ddd52 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 3 Oct 2026 17:19:05 +0000 Subject: [PATCH] Expose safe inheritance origins in explanations --- docs/cli.md | 11 ++- docs/internal/llm-runtime.md | 4 ++ docs/roadmap/implementation.md | 2 + .../framework/llm/runtime_inspection_test.go | 71 +++++++++++++++++++ internal/framework/llm/safe_view.go | 19 +++-- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 2752f3e..dc5bc90 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -74,9 +74,14 @@ YAML summary of selected ID, backend, model, sampling controls, per-attempt timeout, service tier, and reasoning effort for each role. It omits credentials, endpoints, and arbitrary extra provider parameters. Explain writes a YAML sequence of field paths, source origins, and recorded derivations without -resolved setting values. These views share the same settings and profile -validation path as `process`. Unknown views are usage errors. The effective -settings YAML is for inspection and is not guaranteed reloadable. +resolved setting values. An inherited field may also include an `inheritance` +sequence; each step identifies `from`, `to`, and `reference` paths as JSON +pointers. These fields describe the effective inheritance chain without +including configuration values. A direct child override reports its own source +instead. Debug configuration origins use the same explanation shape. These views +share the same settings and profile validation path as `process`. Unknown views +are usage errors. The effective settings YAML is for inspection and is not +guaranteed reloadable. For `process`, `--output` writes the corrected transcript to a file and leaves stdout empty on success. Without it, stdout carries transcript JSON only on diff --git a/docs/internal/llm-runtime.md b/docs/internal/llm-runtime.md index 327676d..4d4c794 100644 --- a/docs/internal/llm-runtime.md +++ b/docs/internal/llm-runtime.md @@ -24,6 +24,10 @@ Counts distinguish provider attempts, transient retries, and structural repairs, including on errors. Secrets from all configured backends, including unselected ones, are available to the diagnostics redaction boundary. +CLI explain output and debug configuration origins share a value-free projection +of effective field origins. The [CLI contract](../cli.md) defines its optional +inheritance evidence fields. + ## Orchestration CLI processing supplies two role clients from the same engine to the runner. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 486f63d..8d2abc1 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -123,6 +123,8 @@ library tests, then commit the focused configuration unit. ## Stage 3 — Expose Compact Inheritance Evidence in Safe Inspection +**Completion:** Complete. + **Primary files:** `internal/framework/llm/safe_view.go`, `internal/framework/llm/runtime_inspection_test.go`, `docs/cli.md`, and the relevant explanation description in `docs/internal/llm-runtime.md` if needed. diff --git a/internal/framework/llm/runtime_inspection_test.go b/internal/framework/llm/runtime_inspection_test.go index 18eec14..765d9fa 100644 --- a/internal/framework/llm/runtime_inspection_test.go +++ b/internal/framework/llm/runtime_inspection_test.go @@ -11,6 +11,7 @@ import ( ck "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/audita/internal/core/config" + "gopkg.in/yaml.v3" ) type rejectingTransport struct{ t *testing.T } @@ -112,6 +113,76 @@ func TestOfflineProfileAssemblyAndSafeViews(t *testing.T) { } } +func TestExplanationYAMLIncludesSafeInheritanceEvidence(t *testing.T) { + t.Setenv("SYNTHETIC_KEY", "synthetic-inherited-secret-never-print") + text := `version: 2 +backends: + shared: + endpoint: https://shared.invalid/v1 + concurrency: 3 + queue_capacity: 0 + credential: SYNTHETIC_KEY + offline: + extends: shared + concurrency: 1 +llm: + proposal_profile: base +` + loaded, err := config.Load(ck.Bytes("config.yml", []byte(text)), "") + if err != nil { + t.Fatal(err) + } + explanation, err := ExplanationYAML(loaded) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(explanation), "synthetic-inherited-secret-never-print") { + t.Fatal("explanation exposed a resolved inherited credential") + } + var records []struct { + Path string `yaml:"path"` + Inheritance []struct { + From string `yaml:"from"` + To string `yaml:"to"` + Reference string `yaml:"reference"` + } `yaml:"inheritance"` + } + if err := yaml.Unmarshal(explanation, &records); err != nil { + t.Fatal(err) + } + var endpoint, credential, concurrency *struct { + Path string `yaml:"path"` + Inheritance []struct { + From string `yaml:"from"` + To string `yaml:"to"` + Reference string `yaml:"reference"` + } `yaml:"inheritance"` + } + for i := range records { + switch records[i].Path { + case "/backends/offline/endpoint": + endpoint = &records[i] + case "/backends/offline/credential": + credential = &records[i] + case "/backends/offline/concurrency": + concurrency = &records[i] + } + } + if endpoint == nil || len(endpoint.Inheritance) != 1 { + t.Fatalf("inherited endpoint evidence missing: %s", explanation) + } + step := endpoint.Inheritance[0] + if step.From != "/backends/shared/endpoint" || step.To != "/backends/offline/endpoint" || step.Reference != "/backends/offline/extends" { + t.Fatalf("inherited endpoint evidence: %+v", step) + } + if credential == nil || len(credential.Inheritance) != 1 { + t.Fatalf("inherited credential evidence missing: %s", explanation) + } + if concurrency == nil || len(concurrency.Inheritance) != 0 { + t.Fatalf("direct child override reported parent inheritance: %+v", concurrency) + } +} + func TestProfileAssemblyRejectsUnsafeSelections(t *testing.T) { t.Setenv("SYNTHETIC_KEY", "synthetic-value-never-print") dir := profileLayout(t, profileBase, "") diff --git a/internal/framework/llm/safe_view.go b/internal/framework/llm/safe_view.go index d55eaf9..c2f0293 100644 --- a/internal/framework/llm/safe_view.go +++ b/internal/framework/llm/safe_view.go @@ -61,11 +61,17 @@ type derivationView struct { Reason string `yaml:"reason"` Dependencies []string `yaml:"dependencies,omitempty"` } +type inheritanceStepView struct { + From string `yaml:"from"` + To string `yaml:"to"` + Reference string `yaml:"reference"` +} type explanationView struct { - Path string `yaml:"path"` - Source string `yaml:"source"` - Layer ck.Layer `yaml:"layer"` - Derivations []derivationView `yaml:"derivations,omitempty"` + Path string `yaml:"path"` + Source string `yaml:"source"` + Layer ck.Layer `yaml:"layer"` + Derivations []derivationView `yaml:"derivations,omitempty"` + Inheritance []inheritanceStepView `yaml:"inheritance,omitempty"` } // ExplanationYAML reports only origins and recorded resolution operations. @@ -93,6 +99,11 @@ func ExplanationYAML(result *ck.Result[config.Config]) ([]byte, error) { continue } record := explanationView{Path: path.String(), Layer: info.Origin.Layer, Source: string(info.Origin.Layer)} + for _, step := range info.Origin.Inheritance { + record.Inheritance = append(record.Inheritance, inheritanceStepView{ + From: step.From.String(), To: step.To.String(), Reference: step.Reference.String(), + }) + } if len(info.Origin.Locations) > 0 { if label := sources[info.Origin.Locations[0].SourceID]; label != "" { record.Source = label