diff --git a/core/schemas/secretvar.go b/core/schemas/secretvar.go index 09b946565a..82b29d4ab4 100644 --- a/core/schemas/secretvar.go +++ b/core/schemas/secretvar.go @@ -1,6 +1,7 @@ package schemas import ( + "bytes" "database/sql/driver" "fmt" "os" @@ -47,8 +48,7 @@ func NewSecretVar(value string) *SecretVar { } // If it's a valid JSON object following the SecretVar schema, unmarshal it if sonic.Valid([]byte(value)) { - valueNode, _ := sonic.Get([]byte(val), "value") - if valueNode.Exists() { + if trimmed := bytes.TrimSpace([]byte(val)); len(trimmed) > 0 && trimmed[0] == '{' { type secretVarCompat struct { Val string `json:"value"` Ref string `json:"ref"` @@ -299,8 +299,7 @@ func (e *SecretVar) UnmarshalJSON(data []byte) error { val = unquoted } if sonic.Valid(data) { - valueNode, _ := sonic.Get(data, "value") - if valueNode.Exists() { + if trimmed := bytes.TrimSpace(data); len(trimmed) > 0 && trimmed[0] == '{' { type secretVarCompat struct { Val string `json:"value"` Ref string `json:"ref"` diff --git a/core/schemas/secretvar_test.go b/core/schemas/secretvar_test.go index 7b2f213ac5..5b6664d6e4 100644 --- a/core/schemas/secretvar_test.go +++ b/core/schemas/secretvar_test.go @@ -114,6 +114,23 @@ func TestSecretVar_UnmarshalJSON_BackwardCompat(t *testing.T) { os.Setenv("MY_KEY", "resolved-value") defer os.Unsetenv("MY_KEY") + t.Run("from_env without value field", func(t *testing.T) { + input := `{"env_var":"MY_KEY","from_env":true}` + var sv SecretVar + if err := sv.UnmarshalJSON([]byte(input)); err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + if sv.GetRawRef() != "env.MY_KEY" { + t.Errorf("expected ref %q, got %q", "env.MY_KEY", sv.GetRawRef()) + } + if !sv.IsFromSecret() { + t.Error("expected IsFromSecret=true") + } + if sv.Val != "resolved-value" { + t.Errorf("expected Val=%q, got %q", "resolved-value", sv.Val) + } + }) + t.Run("old env_var/from_env format", func(t *testing.T) { input := `{"value":"my-api-key","env_var":"env.MY_KEY","from_env":true}` var secretVar SecretVar @@ -221,6 +238,22 @@ func TestNewSecretVar_SecretVarReference(t *testing.T) { } } +func TestNewSecretVar_FromEnvWithoutValueField(t *testing.T) { + os.Setenv("MY_KEY", "resolved-value") + defer os.Unsetenv("MY_KEY") + + sv := NewSecretVar(`{"env_var":"MY_KEY","from_env":true}`) + if sv.GetRawRef() != "env.MY_KEY" { + t.Errorf("expected ref %q, got %q", "env.MY_KEY", sv.GetRawRef()) + } + if !sv.IsFromSecret() { + t.Error("expected IsFromSecret=true") + } + if sv.Val != "resolved-value" { + t.Errorf("expected Val=%q, got %q", "resolved-value", sv.Val) + } +} + // TestSecretVar_RealWorldVertexCredentials tests the actual use case that triggered // the double-escaping bug: Vertex AI service account credentials func TestSecretVar_RealWorldVertexCredentials(t *testing.T) {