From 0a2246bdbe9f17129fd2eb5a59a68b4fb5d2548d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 15 Oct 2026 22:18:57 +0000 Subject: [PATCH] Check import opt-in before processing source headers --- docs/roadmap/remediation.md | 8 ++++++- facade_source_version_mode_test.go | 35 ++++++++++++++++++++++++++++++ internal/engine/load_input.go | 8 ++++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 1e23476..a165647 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -218,7 +218,13 @@ Required remediation: ### 4. Preserve import opt-in before stripping source directives (P2) -Status: open. +Status: complete. Directive opt-in is now checked on the original document +before source-version processing. The shared acquisition boundary covers main +and direct underlay sources. The public regression failed for both enabled +modes and roles before the fix and now verifies `import.disabled`, source-local +attribution, and no partial result. Focused source-version/directive/import +tests and the everyday check passed on `go1.26.9 linux/amd64`; existing enabled +import and selected-overlay tests remain green. Affected: `assembleBaseInputTrackedForRole` in [load_input.go](../../internal/engine/load_input.go), lines 54–87, and diff --git a/facade_source_version_mode_test.go b/facade_source_version_mode_test.go index c801788..ed1ac30 100644 --- a/facade_source_version_mode_test.go +++ b/facade_source_version_mode_test.go @@ -78,6 +78,41 @@ func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) { } } +func TestFacadeSourceVersionModesPreserveImportOptIn(t *testing.T) { + t.Parallel() + for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} { + for _, underlay := range []bool{false, true} { + bad := configkit.Bytes("disabled-imports", []byte("version: accepted\nconfigkit: {imports: [{path: missing.yml}]}\nvalue: visible\n")) + definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ + Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }, + }} + var err error + var resultPresent bool + wantSource := "source-1" + if underlay { + loaded, loadErr := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("version: main\n"))}, configkit.LayeredOptions[map[string]any]{ + Definition: definition, SourceVersionMode: mode, + Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: bad}}}}, + }) + err, resultPresent, wantSource = loadErr, loaded != nil, "source-2" + } else { + loaded, loadErr := configkit.Load(bad, configkit.Options[map[string]any]{Definition: definition, SourceVersionMode: mode}) + err, resultPresent = loadErr, loaded != nil + } + var failure *configkit.LoadError + if resultPresent || !errors.As(err, &failure) { + t.Errorf("mode=%v underlay=%v: import opt-in did not fail safely", mode, underlay) + continue + } + diagnostics := failure.Diagnostics() + if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportDisabled || diagnostics[0].Path.String() != "/configkit" || + diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != wantSource { + t.Errorf("mode=%v underlay=%v: diagnostics=%#v", mode, underlay, diagnostics) + } + } + } +} + func (reader *sourceVersionModeReader) Read(data []byte) (int, error) { reader.reads++ return reader.Reader.Read(data) diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index e46ee51..a8b702f 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -51,6 +51,11 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d } return input, diagnostics } + if !options.Imports && parsed.document != nil { + if diagnostics := disabledDirectiveDiagnostics(parsed.document, Path{}); len(diagnostics) != 0 { + return input, diagnostics + } + } var versionProcessing *sourceVersionProcessingResult if versionState != nil && versionState.mode != sourceVersionModeDefault { var sourceID string @@ -84,9 +89,6 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d } } if !options.Imports && input.document != nil { - if diagnostics := disabledDirectiveDiagnostics(input.document, Path{}); len(diagnostics) != 0 { - return input, diagnostics - } _, placed, err := placeDocument(input.document, destination, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes) if err != nil { return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}