Publish redacted ConfigKit effective configuration

This commit is contained in:
2026-09-13 16:43:32 +00:00
parent d770e19c17
commit 69295d72c9
6 changed files with 125 additions and 10 deletions

View File

@@ -325,6 +325,8 @@ No unchecked narrowing or YAML round-trip remains at these consumers.
## Stage 6 — Prepare Debug Rendering For The ConfigKit Result
**Status:** Completed 2026-09-13.
**Goal:** Prepare the publication boundary before changing command loading.
**Work:**

View File

@@ -39,6 +39,7 @@ func documentDefinition(state *documentLoadState) configkit.Definition[Document]
Path: configkit.Keys("version"),
Check: validateDocumentVersion,
},
Secret: sensitiveDocumentPath,
}
}

View File

@@ -127,6 +127,52 @@ pipelines:
}
}
func TestLoadedConfigRedactsSensitiveNestedOptions(t *testing.T) {
t.Parallel()
loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(`version: 5
pipelines:
session:
input:
module: input
options:
nested:
api_key: input-secret
artifacts:
score:
extract:
module: extract
validators:
- module: configured-a
options:
credentials:
token: validator-secret
`)), LoadOptions{})
if err != nil {
t.Fatalf("loadConfigSource() error = %v", err)
}
var effective bytes.Buffer
if err := loaded.WriteEffective(&effective); err != nil {
t.Fatalf("WriteEffective() error = %v", err)
}
for _, secret := range []string{"input-secret", "validator-secret"} {
if strings.Contains(effective.String(), secret) {
t.Fatalf("effective configuration exposes %q: %q", secret, effective.String())
}
}
if !strings.Contains(effective.String(), "[REDACTED]") {
t.Fatalf("effective configuration did not mark sensitive values: %q", effective.String())
}
var explanation bytes.Buffer
if err := loaded.WriteExplanation(&explanation, "/pipelines/session/artifacts/score/extract/validators/0/options/credentials/token"); err != nil {
t.Fatalf("WriteExplanation() error = %v", err)
}
if strings.Contains(explanation.String(), "validator-secret") || !strings.Contains(explanation.String(), "[REDACTED]") {
t.Fatalf("explanation exposes sensitive value: %q", explanation.String())
}
}
func pipelineProfileWithChangedOption(profile pipeline.PipelineProfile) pipeline.PipelineProfile {
profile = clonePipelineProfile(profile)
nested := profile.Artifacts["score"].Extract.Options["nested"].(map[string]any)

View File

@@ -4,6 +4,7 @@ import (
"reflect"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
@@ -188,3 +189,21 @@ func sensitiveConfigKey(key string) bool {
key = strings.ToLower(key)
return strings.Contains(key, "api_key") || strings.Contains(key, "apikey") || strings.Contains(key, "authorization") || strings.Contains(key, "bearer") || strings.Contains(key, "password") || strings.Contains(key, "secret") || strings.Contains(key, "token")
}
// sensitiveDocumentPath applies the existing option-key classification to the
// fully typed ConfigKit document. Dynamic option maps can be nested below any
// producer or validator, so every key after an options container participates.
func sensitiveDocumentPath(path configkit.Path) bool {
segments := strings.Split(strings.TrimPrefix(path.String(), "/"), "/")
withinOptions := false
for _, segment := range segments {
segment = strings.ReplaceAll(strings.ReplaceAll(segment, "~1", "/"), "~0", "~")
if withinOptions && sensitiveConfigKey(segment) {
return true
}
if segment == "options" {
withinOptions = true
}
}
return false
}

View File

@@ -3,6 +3,8 @@ package debugbundle
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
@@ -92,6 +94,9 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
t.Fatal(err)
}
if err := summary.WriteEffectiveConfig(testEffectiveConfigWriter{content: "version: 5\n"}); err != nil {
t.Fatal(err)
}
if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil {
t.Fatal(err)
}
@@ -120,6 +125,7 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
for _, name := range []string{
ArtifactInvocationMetadata,
ArtifactEffectiveConfig,
ArtifactEffectiveConfigYAML,
ArtifactResolvedPipeline,
ArtifactResolvedReferences,
ArtifactCheckpointEvents,
@@ -139,6 +145,20 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
}
}
func TestWriteEffectiveConfigDoesNotPublishPartialRendering(t *testing.T) {
bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42))
if err != nil {
t.Fatal(err)
}
if err := bundle.Summary().WriteEffectiveConfig(testEffectiveConfigWriter{err: errors.New("render failed")}); err == nil {
t.Fatal("WriteEffectiveConfig() error = nil, want render failure")
}
path := filepath.Join(bundle.SummaryRoot(), ArtifactEffectiveConfigYAML)
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("effective configuration artifact exists after rendering failure: %v", err)
}
}
func TestWriteInvocationPreservesReasoningEffortOverrideStates(t *testing.T) {
replacement := "focused"
cleared := ""
@@ -222,6 +242,19 @@ func (testRedactedSummaryPayload) RedactedSummaryPayload() any {
return map[string]any{"redacted": true}
}
type testEffectiveConfigWriter struct {
content string
err error
}
func (writer testEffectiveConfigWriter) WriteEffective(output io.Writer) error {
if writer.err != nil {
return writer.err
}
_, err := io.WriteString(output, writer.content)
return err
}
type testRedactedResolvedPipelinePayload struct{}
func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {

View File

@@ -1,7 +1,9 @@
package debugbundle
import (
"bytes"
"fmt"
"io"
"strings"
"time"
@@ -12,19 +14,21 @@ import (
)
const (
ArtifactInvocationMetadata = "invocation.json"
ArtifactEffectiveConfig = "effective-config.json"
ArtifactResolvedPipeline = "resolved-pipeline.json"
ArtifactResolvedReferences = "resolved-references.json"
ArtifactCheckpointEvents = "checkpoint-events.json"
ArtifactRunManifest = "run-manifest.json"
ArtifactChunkPlan = "chunk-plan.json"
ArtifactRunReport = "run-report.json"
ArtifactDiagnostics = "final-diagnostics.json"
ArtifactErrorLog = "error.log"
ArtifactInvocationMetadata = "invocation.json"
ArtifactEffectiveConfig = "effective-config.json"
ArtifactEffectiveConfigYAML = "effective-config.yml"
ArtifactResolvedPipeline = "resolved-pipeline.json"
ArtifactResolvedReferences = "resolved-references.json"
ArtifactCheckpointEvents = "checkpoint-events.json"
ArtifactRunManifest = "run-manifest.json"
ArtifactChunkPlan = "chunk-plan.json"
ArtifactRunReport = "run-report.json"
ArtifactDiagnostics = "final-diagnostics.json"
ArtifactErrorLog = "error.log"
)
type RedactedSummaryPayload interface{ RedactedSummaryPayload() any }
type EffectiveConfigWriter interface{ WriteEffective(io.Writer) error }
type RedactedResolvedPipelinePayload interface {
RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline
}
@@ -86,6 +90,16 @@ func (w *SummaryWriter) WriteRedactedEffectiveConfig(payload RedactedSummaryPayl
}
return w.writeJSON(ArtifactEffectiveConfig, payload.RedactedSummaryPayload())
}
func (w *SummaryWriter) WriteEffectiveConfig(payload EffectiveConfigWriter) error {
if payload == nil {
return fmt.Errorf("effective configuration writer must not be nil")
}
var rendered bytes.Buffer
if err := payload.WriteEffective(&rendered); err != nil {
return fmt.Errorf("render effective configuration: %w", err)
}
return w.writeBytes(ArtifactEffectiveConfigYAML, rendered.Bytes())
}
func (w *SummaryWriter) WriteResolvedPipeline(payload RedactedResolvedPipelinePayload) error {
if payload == nil {
return fmt.Errorf("redacted resolved pipeline payload must not be nil")