Expose safe inheritance origins in explanations

This commit is contained in:
2026-10-03 17:19:05 +00:00
parent 1cfe0c5a01
commit b64010e729
5 changed files with 100 additions and 7 deletions

View File

@@ -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, timeout, service tier, and reasoning effort for each role. It omits credentials,
endpoints, and arbitrary extra provider parameters. Explain writes a YAML endpoints, and arbitrary extra provider parameters. Explain writes a YAML
sequence of field paths, source origins, and recorded derivations without sequence of field paths, source origins, and recorded derivations without
resolved setting values. These views share the same settings and profile resolved setting values. An inherited field may also include an `inheritance`
validation path as `process`. Unknown views are usage errors. The effective sequence; each step identifies `from`, `to`, and `reference` paths as JSON
settings YAML is for inspection and is not guaranteed reloadable. 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 For `process`, `--output` writes the corrected transcript to a file and leaves
stdout empty on success. Without it, stdout carries transcript JSON only on stdout empty on success. Without it, stdout carries transcript JSON only on

View File

@@ -24,6 +24,10 @@ Counts distinguish provider attempts, transient retries, and structural
repairs, including on errors. Secrets from all configured backends, including unselected repairs, including on errors. Secrets from all configured backends, including unselected
ones, are available to the diagnostics redaction boundary. 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 ## Orchestration
CLI processing supplies two role clients from the same engine to the runner. CLI processing supplies two role clients from the same engine to the runner.

View File

@@ -123,6 +123,8 @@ library tests, then commit the focused configuration unit.
## Stage 3 — Expose Compact Inheritance Evidence in Safe Inspection ## Stage 3 — Expose Compact Inheritance Evidence in Safe Inspection
**Completion:** Complete.
**Primary files:** `internal/framework/llm/safe_view.go`, **Primary files:** `internal/framework/llm/safe_view.go`,
`internal/framework/llm/runtime_inspection_test.go`, `docs/cli.md`, and the `internal/framework/llm/runtime_inspection_test.go`, `docs/cli.md`, and the
relevant explanation description in `docs/internal/llm-runtime.md` if needed. relevant explanation description in `docs/internal/llm-runtime.md` if needed.

View File

@@ -11,6 +11,7 @@ import (
ck "gitea.maximumdirect.net/eric/ConfigKit" ck "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/audita/internal/core/config" "gitea.maximumdirect.net/eric/audita/internal/core/config"
"gopkg.in/yaml.v3"
) )
type rejectingTransport struct{ t *testing.T } 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) { func TestProfileAssemblyRejectsUnsafeSelections(t *testing.T) {
t.Setenv("SYNTHETIC_KEY", "synthetic-value-never-print") t.Setenv("SYNTHETIC_KEY", "synthetic-value-never-print")
dir := profileLayout(t, profileBase, "") dir := profileLayout(t, profileBase, "")

View File

@@ -61,11 +61,17 @@ type derivationView struct {
Reason string `yaml:"reason"` Reason string `yaml:"reason"`
Dependencies []string `yaml:"dependencies,omitempty"` Dependencies []string `yaml:"dependencies,omitempty"`
} }
type inheritanceStepView struct {
From string `yaml:"from"`
To string `yaml:"to"`
Reference string `yaml:"reference"`
}
type explanationView struct { type explanationView struct {
Path string `yaml:"path"` Path string `yaml:"path"`
Source string `yaml:"source"` Source string `yaml:"source"`
Layer ck.Layer `yaml:"layer"` Layer ck.Layer `yaml:"layer"`
Derivations []derivationView `yaml:"derivations,omitempty"` Derivations []derivationView `yaml:"derivations,omitempty"`
Inheritance []inheritanceStepView `yaml:"inheritance,omitempty"`
} }
// ExplanationYAML reports only origins and recorded resolution operations. // ExplanationYAML reports only origins and recorded resolution operations.
@@ -93,6 +99,11 @@ func ExplanationYAML(result *ck.Result[config.Config]) ([]byte, error) {
continue continue
} }
record := explanationView{Path: path.String(), Layer: info.Origin.Layer, Source: string(info.Origin.Layer)} 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 len(info.Origin.Locations) > 0 {
if label := sources[info.Origin.Locations[0].SourceID]; label != "" { if label := sources[info.Origin.Locations[0].SourceID]; label != "" {
record.Source = label record.Source = label