diff --git a/docs/config.md b/docs/config.md index 32c0a79..42419bb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -243,7 +243,9 @@ public-name representation, JSON marshals only the name, and projection, metadat histories, diagnostics, effective output, and resolution snapshots never retain the credential. Validation remains read-only: in addition to ordinary projected-value comparison, ConfigKit checks the private -resolved state against this load's lookup result. Applications must keep their +resolved state against this load's lookup result. Validator mutation checks also +reject changes to projected field sensitivity, even when a replacement dynamic +concrete type preserves the same textual values. Applications must keep their validation messages value-free and must not include `Value()` in diagnostics. For example, a typed pipeline field is declared as: diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 96fd230..e4a6a67 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -2,18 +2,18 @@ ## Status and scope -Status: in progress. Review baseline: `f42ecc70c330a399aa6dada87658694402a515ec`. +Status: complete. All nine findings have been addressed. Review baseline: `f42ecc70c330a399aa6dada87658694402a515ec`. This review covers the initial implementation against the [architecture policy](../policy/architecture.md), [configuration contract](../config.md), and [implementation plan](implementation.md). The package boundaries and principal -lifecycle are in place, but the findings below must be addressed before claiming -that the initial target is fully satisfied. No production code was changed during -this review. All findings below were reproduced through the public API or local +lifecycle were in place at review time; the original findings below are retained +with completion evidence. No production code was changed during the original +review; subsequent remediation is recorded here. All findings below were reproduced through the public API or local validation tools, with the additional affected call sites identified by inspection. Priorities: P1 means high-impact confidentiality, isolation, or availability; P2 means a correctness or validation gap that also requires remediation. -Each item remains open until its regression checks and the relevant baseline pass. +Each item below records its completed correction and validation evidence. Keep current contracts and implementation evidence accurate when fixing these issues; do not change the agreed semantics simply to accommodate the implementation. @@ -21,7 +21,10 @@ do not change the agreed semantics simply to accommodate the implementation. Status: complete. Typed projections now retain concrete field sensitivity; final, baseline and pre-resolution projections feed historical/dependency classification. -Dynamic nested, replaced, deleted and introduced-value regressions pass. Full tests +Dynamic nested, replaced, deleted and introduced-value regressions pass. A final +audit additionally verified that validators cannot change dynamic field sensitivity +through equal-text type replacement; the added regression failed before the fix +and now passes. Full tests pass with the temporary canonical-TMPDIR workaround for issue 8; vet, build and whitespace checks pass. @@ -285,3 +288,24 @@ precedence, separate explicit bootstrap, and application-owned domain lifecycle remain appropriate. Repair these boundary and consistency defects within that architecture. Re-run focused regressions, the normal baseline and race suites, local documentation checks, and applicable release gates before closing this roadmap. + +## Remediation completion + +All nine findings are corrected and committed in individual units of work, with an +additional issue-1 audit correction for validator sensitivity mutation. The normal +macOS baseline no longer requires a TMPDIR workaround. The historical review and +intermediate validation notes above retain the conditions that applied at the time. + +The complete source checker is validated locally with: + +```sh +CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5 CONFIGKIT_NEWER_TOOLCHAIN=go1.26.0 /bin/bash scripts/release-check.sh +``` + +This uses the available cached toolchains, runs both native baselines and the race +suite, checks module hygiene, formatting, documentation and example boundaries, +and cross-builds Linux/macOS on amd64/arm64. Native execution during remediation +was macOS/arm64; new native Linux evidence and selection of current supported patch +releases remain maintainer release gates. No release, tag, push or downstream +application migration was performed. Retire this completed roadmap during release +preparation according to the documentation policy. diff --git a/render_test.go b/render_test.go index adb7b2e..dad22e5 100644 --- a/render_test.go +++ b/render_test.go @@ -226,3 +226,25 @@ func TestDynamicConcreteTagsRemainSecretThroughHistory(t *testing.T) { }) } } + +func TestValidatorCannotChangeDynamicSensitivityWithoutChangingText(t *testing.T) { + type sensitive struct { + Token string `yaml:"token" configkit:"secret"` + } + type cfg struct { + Payload any `yaml:"payload"` + } + _, err := Load(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) { + c.Payload = sensitive{Token: c.Payload.(map[string]any)["token"].(string)} + }}}) + var failure *LoadError + if !errors.As(err, &failure) { + t.Fatal("validator changed inspection sensitivity without a mutation error") + } + for _, d := range failure.Diagnostics() { + if d.Code == CodeValidateMutation { + return + } + } + t.Fatal("missing validation mutation diagnostic") +} diff --git a/secret.go b/secret.go index e844170..215221b 100644 --- a/secret.go +++ b/secret.go @@ -139,3 +139,26 @@ func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) { } return Path{}, false } + +// Dynamic type replacement can change classification even when projected text +// is equal. Validators must preserve this inspection metadata as well as values. +func projectedSensitivity(root *document.Node) map[string]Path { + result := make(map[string]Path) + var visit func(*document.Node, Path) + visit = func(node *document.Node, path Path) { + if node == nil { + return + } + if node.Sensitive { + result[path.String()] = path.clone() + } + for _, entry := range node.Mapping { + visit(entry.Value, path.Key(entry.Key)) + } + for i, child := range node.Sequence { + visit(child, path.Index(i)) + } + } + visit(root, Path{}) + return result +} diff --git a/validation.go b/validation.go index 6857123..cf1bd77 100644 --- a/validation.go +++ b/validation.go @@ -112,6 +112,18 @@ func runValidation[T any](configuration *T, validator func(*T, *Validation), sch for _, delta := range compareDocumentTrees(before, after) { state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(delta.path)) } + beforeSensitivity := projectedSensitivity(before) + afterSensitivity := projectedSensitivity(after) + for pointer, path := range beforeSensitivity { + if _, exists := afterSensitivity[pointer]; !exists { + state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(path)) + } + } + for pointer, path := range afterSensitivity { + if _, exists := beforeSensitivity[pointer]; !exists { + state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(path)) + } + } for _, path := range changedSecretStates(reflect.ValueOf(*configuration), schema.root, lookups) { state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(path)) }