From d32674937a4f1852bc52b8d010e1b9fb353473e4 Mon Sep 17 00:00:00 2001 From: tanmay11k Date: Fri, 20 Feb 2026 23:55:15 -0500 Subject: [PATCH] feat: openclaw-style secrets (env.vars + \) and per-task model routing - Replace python-dotenv with config.json env.vars block + \ substitution - Add models section for per-task model routing (heartbeat, subagent, default) - Heartbeat/subagent tasks can use different models/providers than main chat - Remove python-dotenv from dependencies - Update all docs to reflect new config approach - Reorganize docs into project/ and research/ subdirectories --- config.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/config.py b/config.py index 9d7682b..dc7adc3 100644 --- a/config.py +++ b/config.py @@ -583,6 +583,7 @@ def save_default_config() -> str: os.makedirs(CONFIG_DIR, exist_ok=True) if os.path.isfile(CONFIG_PATH): + migrate_config() return CONFIG_PATH default = { @@ -689,6 +690,65 @@ def save_default_config() -> str: return CONFIG_PATH +def migrate_config() -> bool: + """Patch an existing config.json with new sections added in later versions. + + Called on every startup (via ``save_default_config``). Only touches the + file when keys are actually missing — existing values are never + overwritten. Returns ``True`` if the file was modified. + """ + if not os.path.isfile(CONFIG_PATH): + return False + + try: + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError): + return False + + changed = False + + # --- env.vars block (added when secrets moved from .env) --------------- + if "env" not in data: + data["env"] = { + "vars": { + "SLACK_BOT_TOKEN": "", + "SLACK_APP_TOKEN": "", + "TELEGRAM_BOT_TOKEN": "", + "DISCORD_BOT_TOKEN": "", + "ANTHROPIC_API_KEY": "", + "OPENCODE_SERVER_PASSWORD": "", + } + } + changed = True + elif "vars" not in data.get("env", {}): + data["env"]["vars"] = {} + changed = True + + # --- bot_token fields in adapter sections ------------------------------ + for section, token_key, env_ref in [ + ("slack", "bot_token", "${SLACK_BOT_TOKEN}"), + ("slack", "app_token", "${SLACK_APP_TOKEN}"), + ("telegram", "bot_token", "${TELEGRAM_BOT_TOKEN}"), + ("discord", "bot_token", "${DISCORD_BOT_TOKEN}"), + ]: + if section in data and token_key not in data[section]: + data[section][token_key] = env_ref + changed = True + + # --- models section (per-task model routing) --------------------------- + if "models" not in data: + data["models"] = {"heartbeat": None, "subagent": None, "default": None} + changed = True + + if changed: + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + logger.info(f"Config migrated with new sections: {CONFIG_PATH}") + + return changed + + def write_mcp_config(mcp_config: MCPConfig, workspace_dir: str, use_claude: bool) -> None: """Write MCP server config to the appropriate file for the runtime.