feat(config): add {env:VAR} syntax for environment variable references

Add secret_resolver module to resolve {env:VAR_NAME} placeholders in
config files. Allows storing sensitive values like API keys in
environment variables instead of config.json.
This commit is contained in:
chengyongru 2026-03-18 23:57:41 +08:00
parent 214bf66a29
commit beaa8de2c5
2 changed files with 50 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import json
from pathlib import Path
from nanobot.config.schema import Config
from nanobot.config.secret_resolver import resolve_config
# Global variable to store current config path (for multi-instance support)
@ -40,6 +41,7 @@ def load_config(config_path: Path | None = None) -> Config:
with open(path, encoding="utf-8") as f:
data = json.load(f)
data = _migrate_config(data)
data = resolve_config(data) # Resolve {env:VAR} references
return Config.model_validate(data)
except (json.JSONDecodeError, ValueError) as e:
print(f"Warning: Failed to load config from {path}: {e}")

View File

@ -0,0 +1,48 @@
"""Secret reference resolver for configuration.
Supports {env:VARIABLE_NAME} syntax to reference environment variables.
"""
import os
import re
# Pattern matches {env:VAR_NAME} where VAR_NAME follows env var naming conventions
_REF_PATTERN = re.compile(r"\{env:([A-Z_][A-Z0-9_]*)\}")
def resolve_env_vars(value: str) -> str:
"""Resolve {env:VAR} references in a string.
Args:
value: String that may contain {env:VAR} references.
Returns:
String with all {env:VAR} references replaced by their values.
Unresolved references are left unchanged.
"""
def replacer(match: re.Match[str]) -> str:
var_name = match.group(1)
env_value = os.environ.get(var_name)
if env_value is None:
return match.group(0) # Keep original if env var doesn't exist
return env_value
return _REF_PATTERN.sub(replacer, value)
def resolve_config(obj):
"""Recursively resolve {env:VAR} references in a configuration object.
Args:
obj: Configuration value (str, dict, list, or other).
Returns:
Configuration with all {env:VAR} references resolved.
"""
if isinstance(obj, str):
return resolve_env_vars(obj)
if isinstance(obj, dict):
return {k: resolve_config(v) for k, v in obj.items()}
if isinstance(obj, list):
return [resolve_config(item) for item in obj]
return obj