Publish redacted ConfigKit effective configuration
This commit is contained in:
@@ -325,6 +325,8 @@ No unchecked narrowing or YAML round-trip remains at these consumers.
|
|||||||
|
|
||||||
## Stage 6 — Prepare Debug Rendering For The ConfigKit Result
|
## Stage 6 — Prepare Debug Rendering For The ConfigKit Result
|
||||||
|
|
||||||
|
**Status:** Completed 2026-09-13.
|
||||||
|
|
||||||
**Goal:** Prepare the publication boundary before changing command loading.
|
**Goal:** Prepare the publication boundary before changing command loading.
|
||||||
|
|
||||||
**Work:**
|
**Work:**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func documentDefinition(state *documentLoadState) configkit.Definition[Document]
|
|||||||
Path: configkit.Keys("version"),
|
Path: configkit.Keys("version"),
|
||||||
Check: validateDocumentVersion,
|
Check: validateDocumentVersion,
|
||||||
},
|
},
|
||||||
|
Secret: sensitiveDocumentPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
func pipelineProfileWithChangedOption(profile pipeline.PipelineProfile) pipeline.PipelineProfile {
|
||||||
profile = clonePipelineProfile(profile)
|
profile = clonePipelineProfile(profile)
|
||||||
nested := profile.Artifacts["score"].Extract.Options["nested"].(map[string]any)
|
nested := profile.Artifacts["score"].Extract.Options["nested"].(map[string]any)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,3 +189,21 @@ func sensitiveConfigKey(key string) bool {
|
|||||||
key = strings.ToLower(key)
|
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")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package debugbundle
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -92,6 +94,9 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
|
|||||||
if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
|
if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := summary.WriteEffectiveConfig(testEffectiveConfigWriter{content: "version: 5\n"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil {
|
if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -120,6 +125,7 @@ func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
|
|||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
ArtifactInvocationMetadata,
|
ArtifactInvocationMetadata,
|
||||||
ArtifactEffectiveConfig,
|
ArtifactEffectiveConfig,
|
||||||
|
ArtifactEffectiveConfigYAML,
|
||||||
ArtifactResolvedPipeline,
|
ArtifactResolvedPipeline,
|
||||||
ArtifactResolvedReferences,
|
ArtifactResolvedReferences,
|
||||||
ArtifactCheckpointEvents,
|
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) {
|
func TestWriteInvocationPreservesReasoningEffortOverrideStates(t *testing.T) {
|
||||||
replacement := "focused"
|
replacement := "focused"
|
||||||
cleared := ""
|
cleared := ""
|
||||||
@@ -222,6 +242,19 @@ func (testRedactedSummaryPayload) RedactedSummaryPayload() any {
|
|||||||
return map[string]any{"redacted": true}
|
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{}
|
type testRedactedResolvedPipelinePayload struct{}
|
||||||
|
|
||||||
func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {
|
func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package debugbundle
|
package debugbundle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,19 +14,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ArtifactInvocationMetadata = "invocation.json"
|
ArtifactInvocationMetadata = "invocation.json"
|
||||||
ArtifactEffectiveConfig = "effective-config.json"
|
ArtifactEffectiveConfig = "effective-config.json"
|
||||||
ArtifactResolvedPipeline = "resolved-pipeline.json"
|
ArtifactEffectiveConfigYAML = "effective-config.yml"
|
||||||
ArtifactResolvedReferences = "resolved-references.json"
|
ArtifactResolvedPipeline = "resolved-pipeline.json"
|
||||||
ArtifactCheckpointEvents = "checkpoint-events.json"
|
ArtifactResolvedReferences = "resolved-references.json"
|
||||||
ArtifactRunManifest = "run-manifest.json"
|
ArtifactCheckpointEvents = "checkpoint-events.json"
|
||||||
ArtifactChunkPlan = "chunk-plan.json"
|
ArtifactRunManifest = "run-manifest.json"
|
||||||
ArtifactRunReport = "run-report.json"
|
ArtifactChunkPlan = "chunk-plan.json"
|
||||||
ArtifactDiagnostics = "final-diagnostics.json"
|
ArtifactRunReport = "run-report.json"
|
||||||
ArtifactErrorLog = "error.log"
|
ArtifactDiagnostics = "final-diagnostics.json"
|
||||||
|
ArtifactErrorLog = "error.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedactedSummaryPayload interface{ RedactedSummaryPayload() any }
|
type RedactedSummaryPayload interface{ RedactedSummaryPayload() any }
|
||||||
|
type EffectiveConfigWriter interface{ WriteEffective(io.Writer) error }
|
||||||
type RedactedResolvedPipelinePayload interface {
|
type RedactedResolvedPipelinePayload interface {
|
||||||
RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline
|
RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline
|
||||||
}
|
}
|
||||||
@@ -86,6 +90,16 @@ func (w *SummaryWriter) WriteRedactedEffectiveConfig(payload RedactedSummaryPayl
|
|||||||
}
|
}
|
||||||
return w.writeJSON(ArtifactEffectiveConfig, payload.RedactedSummaryPayload())
|
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 {
|
func (w *SummaryWriter) WriteResolvedPipeline(payload RedactedResolvedPipelinePayload) error {
|
||||||
if payload == nil {
|
if payload == nil {
|
||||||
return fmt.Errorf("redacted resolved pipeline payload must not be nil")
|
return fmt.Errorf("redacted resolved pipeline payload must not be nil")
|
||||||
|
|||||||
Reference in New Issue
Block a user