From beaa8de2c5dc4b272072dab4175835d33765e6ae Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Wed, 18 Mar 2026 23:57:41 +0800 Subject: [PATCH] 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. --- nanobot/config/loader.py | 2 ++ nanobot/config/secret_resolver.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 nanobot/config/secret_resolver.py diff --git a/nanobot/config/loader.py b/nanobot/config/loader.py index 7d309e5af..eaca1cf9e 100644 --- a/nanobot/config/loader.py +++ b/nanobot/config/loader.py @@ -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}") diff --git a/nanobot/config/secret_resolver.py b/nanobot/config/secret_resolver.py new file mode 100644 index 000000000..dac19c1b5 --- /dev/null +++ b/nanobot/config/secret_resolver.py @@ -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