Skip to content

Commit 766f782

Browse files
committed
Fix API Key Interpolation: Support SecretStr in SecretManager
- Updated SecretManager.resolve_object to unwrap, resolve, and re-wrap SecretStr objects - Updated tests/unit/test_secrets.py to verify SecretManager resolution - Ensures \ patterns in SecretStr fields (like api_key) are correctly resolved
1 parent 059d90e commit 766f782

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

packages/core/src/nl2sql/secrets/manager.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,35 @@ def resolve(self, secret_ref: str) -> str:
8282

8383
raise ValueError(f"Secret not found: {secret_ref}")
8484

85+
return obj
86+
8587
def resolve_object(self, obj: Any) -> Any:
8688
"""
87-
Recursively resolves secret references in a generic object (Pydantic model, dict, list, str).
89+
Recursively resolves secret references in a generic object (Pydantic model, dict, list, str, SecretStr).
8890
Returns a new object with secrets resolved.
8991
"""
90-
from pydantic import BaseModel
92+
from pydantic import BaseModel, SecretStr
9193

9294
# Base case: String
9395
if isinstance(obj, str):
9496
if obj.startswith("${") and obj.endswith("}"):
9597
return self.resolve(obj)
9698
return obj
99+
100+
# Handle SecretStr
101+
if isinstance(obj, SecretStr):
102+
secret_val = obj.get_secret_value()
103+
if secret_val and secret_val.startswith("${") and secret_val.endswith("}"):
104+
resolved_val = self.resolve(secret_val)
105+
return SecretStr(resolved_val)
106+
return obj
97107

98108
# Pydantic Model
99109
if isinstance(obj, BaseModel):
100110
# Recursively resolve fields
101111
updates = {}
102-
for field_name in obj.model_fields.keys():
112+
# Access model_fields from the class, not the instance
113+
for field_name in type(obj).model_fields.keys():
103114
val = getattr(obj, field_name)
104115
resolved = self.resolve_object(val)
105116
# Only update if changed (optimization)

packages/core/tests/unit/test_secrets.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,30 @@ def test_config_hides_secrets():
5555
database="db"
5656
)
5757

58+
5859
assert "hidden_password" not in str(config)
5960
assert "**********" in str(config)
61+
62+
def test_llm_registry_resolves_secrets():
63+
"""Verify SecretManager resolves SecretStr within Pydantic models."""
64+
from nl2sql.secrets import secret_manager
65+
from nl2sql.configs import LLMFileConfig, AgentConfig
66+
67+
config = LLMFileConfig(
68+
default=AgentConfig(
69+
provider="openai",
70+
model="gpt-4o",
71+
api_key=SecretStr("${env:TEST_LLM_KEY}")
72+
)
73+
)
74+
75+
# Mock resolve
76+
with patch("nl2sql.secrets.manager.SecretManager.resolve") as mock_resolve:
77+
mock_resolve.return_value = "sk-resolved-key"
78+
79+
# Test resolve_object directly (ConfigManager uses this)
80+
resolved_config = secret_manager.resolve_object(config)
81+
82+
# Verify resolution happened inside SecretStr
83+
assert resolved_config.default.api_key.get_secret_value() == "sk-resolved-key"
84+
mock_resolve.assert_called_with("${env:TEST_LLM_KEY}")

0 commit comments

Comments
 (0)