From 4e2059512151101489c89d3ef3fae89d72f34a15 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:38:20 -0400 Subject: [PATCH 1/2] fix(env): the runtime environment file admits a configuration document carrier core names a structured configuration document's carrier with a lowercase hex SHA-256 (resources.ConfigurationDocumentKey), and LoadRuntimeEnvironmentFile accepted only [A-Z0-9_] after CODEFLY__, so an --output-env artifact carrying one (a composition's workspace fixture-composition document) could never load. Admit exactly that shape: a document prefix and 64 lowercase hex digits. The name is kept rather than upper-cased in core because this SDK, and every consumer built on an earlier core, finds the document by calling that same function and looking the result up exactly. Co-Authored-By: Claude Fable 5.1 --- runtime_environment_file.go | 37 ++++++++++++++++++ runtime_environment_file_test.go | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/runtime_environment_file.go b/runtime_environment_file.go index c91c3bb..a1b3631 100644 --- a/runtime_environment_file.go +++ b/runtime_environment_file.go @@ -2,11 +2,15 @@ package codefly import ( "bufio" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" "os" "strings" + + "github.com/codefly-dev/core/resources" ) const ( @@ -89,6 +93,9 @@ func validRuntimeEnvironmentName(name string) bool { if !strings.HasPrefix(name, "CODEFLY__") || len(name) == len("CODEFLY__") { return false } + if isConfigurationDocumentCarrier(name) { + return true + } for _, character := range name { switch { case character >= 'A' && character <= 'Z': @@ -100,3 +107,33 @@ func validRuntimeEnvironmentName(name string) bool { } return true } + +// isConfigurationDocumentCarrier reports a structured configuration document's +// carrier exactly as core mints it (resources.ConfigurationDocumentKey): one of +// the two document prefixes and a lowercase hex SHA-256, nothing else. +// +// It is the one name outside [A-Z0-9_] an artifact carries. The digest is +// lowercase because core spells it so, and this SDK finds the document by +// calling that same function and looking the result up exactly +// (configurationDocument), as does every consumer built on an earlier core. +// So the grammar admits the name core mints rather than the name changing +// under its readers, and only that shape: a lowercase letter anywhere else is +// still refused. +func isConfigurationDocumentCarrier(name string) bool { + for _, prefix := range []string{resources.ConfigurationDocumentPrefix, resources.SecretConfigurationDocumentPrefix} { + digest, found := strings.CutPrefix(name, prefix) + if !found { + continue + } + if len(digest) != hex.EncodedLen(sha256.Size) { + return false + } + for _, character := range digest { + if (character < '0' || character > '9') && (character < 'a' || character > 'f') { + return false + } + } + return true + } + return false +} diff --git a/runtime_environment_file_test.go b/runtime_environment_file_test.go index 949ac37..89700e6 100644 --- a/runtime_environment_file_test.go +++ b/runtime_environment_file_test.go @@ -1,10 +1,14 @@ package codefly_test import ( + "context" "os" "path/filepath" + "strings" "testing" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/resources" codefly "github.com/codefly-dev/sdk-go" "github.com/stretchr/testify/require" ) @@ -49,6 +53,69 @@ func TestLoadRuntimeEnvironmentFileRejectsUnsafeFile(t *testing.T) { require.Error(t, codefly.LoadRuntimeEnvironmentFile(link)) } +// TestLoadRuntimeEnvironmentFileCarriesAConfigurationDocument: core names a +// structured configuration document's carrier with a lowercase hex digest +// (resources.ConfigurationDocumentKey), and an artifact exporting one — a +// composition's workspace fixture-composition document — was refused whole. +// The document's carrier now loads, and the SDK reads the document from it. +func TestLoadRuntimeEnvironmentFileCarriesAConfigurationDocument(t *testing.T) { + t.Cleanup(func() { require.NoError(t, codefly.LoadEnvironmentVariables()) }) + t.Setenv(resources.EnvironmentPrefix, "local") + manager := resources.NewEnvironmentVariableManager() + manager.SetEnvironment(&basev0.Environment{Name: "local"}) + require.NoError(t, manager.AddConfigurations(context.Background(), &basev0.Configuration{ + Origin: resources.ConfigurationWorkspace, + Infos: []*basev0.ConfigurationInformation{{ + Name: "fixture-composition", + Data: &basev0.ConfigurationData{Kind: "json", Content: []byte(`{"roles":[]}`)}, + }}, + })) + variables, err := manager.All() + require.NoError(t, err) + key := resources.ConfigurationDocumentKey(resources.ConfigurationWorkspace, "fixture-composition", "local", false) + var body strings.Builder + for _, variable := range variables { + if variable.Key == key { + body.WriteString(variable.Key + "=" + variable.ValueAsString() + "\n") + } + } + require.NotEmpty(t, body.String(), "core delivers the document under the key it names") + t.Setenv(key, "") + + require.NoError(t, codefly.LoadRuntimeEnvironmentFile(writeRuntimeEnvironmentFile(t, body.String(), 0o600))) + require.NoError(t, codefly.LoadEnvironmentVariables()) + document, err := codefly.For(context.Background()).WorkspaceConfigurationDocument("fixture-composition") + require.NoError(t, err) + require.JSONEq(t, `{"roles":[]}`, string(document)) +} + +// TestLoadRuntimeEnvironmentFileAdmitsOnlyTheDocumentDigestInLowerCase holds +// the exception to exactly the shape core mints. +func TestLoadRuntimeEnvironmentFileAdmitsOnlyTheDocumentDigestInLowerCase(t *testing.T) { + digest := strings.Repeat("0a", 32) + for name, admitted := range map[string]bool{ + resources.ConfigurationDocumentPrefix + digest: true, + resources.SecretConfigurationDocumentPrefix + digest: true, + resources.ConfigurationDocumentPrefix + strings.ToUpper(digest): true, + resources.ConfigurationDocumentPrefix + digest[1:]: false, + resources.ConfigurationDocumentPrefix + digest + "a": false, + resources.ConfigurationDocumentPrefix + strings.Repeat("g", 64): false, + "CODEFLY__CONFIGURATION_DOCUMENT_V2__" + digest: false, + "CODEFLY__WORKSPACE_CONFIGURATION__" + digest: false, + "CODEFLY__lower": false, + "codefly__CONFIGURATION_DOCUMENT_V1__" + digest: false, + } { + path := writeRuntimeEnvironmentFile(t, name+"=value\n", 0o600) + t.Setenv(name, "") + err := codefly.LoadRuntimeEnvironmentFile(path) + if admitted { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + func writeRuntimeEnvironmentFile( t *testing.T, body string, From 5cec7552b5e5e6a1ed0c88a913bf700682074e56 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:48:04 -0400 Subject: [PATCH 2/2] fix(env): take the document digest length from core, hash nothing here The reader imported crypto/sha256 and encoding/hex only to spell a length, which the deny-by-default import gate (scripts/allowed-imports.txt) refuses outside the files that hash. Read the length from core's own ConfigurationDocumentKey instead: no new import, and it follows core if the digest ever changes. Co-Authored-By: Claude Fable 5.1 --- runtime_environment_file.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/runtime_environment_file.go b/runtime_environment_file.go index a1b3631..4f06f30 100644 --- a/runtime_environment_file.go +++ b/runtime_environment_file.go @@ -2,8 +2,6 @@ package codefly import ( "bufio" - "crypto/sha256" - "encoding/hex" "errors" "fmt" "io" @@ -108,6 +106,11 @@ func validRuntimeEnvironmentName(name string) bool { return true } +// configurationDocumentDigestLength is the length of the digest core appends +// to a document prefix, read from core's own key rather than recomputed: this +// package hashes nothing (scripts/allowed-imports.txt). +var configurationDocumentDigestLength = len(resources.ConfigurationDocumentKey("", "", "", false)) - len(resources.ConfigurationDocumentPrefix) + // isConfigurationDocumentCarrier reports a structured configuration document's // carrier exactly as core mints it (resources.ConfigurationDocumentKey): one of // the two document prefixes and a lowercase hex SHA-256, nothing else. @@ -125,7 +128,7 @@ func isConfigurationDocumentCarrier(name string) bool { if !found { continue } - if len(digest) != hex.EncodedLen(sha256.Size) { + if len(digest) != configurationDocumentDigestLength { return false } for _, character := range digest {