diff --git a/nanobot/config/loader.py b/nanobot/config/loader.py index fd7d1e8..e6899d3 100644 --- a/nanobot/config/loader.py +++ b/nanobot/config/loader.py @@ -12,35 +12,53 @@ def get_config_path() -> Path: return Path.home() / ".nanobot" / "config.json" +def _get_oauth_store_dir() -> Path: + """Get the OAuth store directory.""" + return Path.home() / ".nanobot" + + def get_data_dir() -> Path: """Get the nanobot data directory.""" from nanobot.utils.helpers import get_data_path return get_data_path() +def _inject_oauth_credentials(config: Config) -> Config: + """Inject OAuth credentials from store into config if available.""" + from nanobot.config.oauth_store import OAuthStore + + store = OAuthStore(_get_oauth_store_dir()) + creds = store.load("anthropic") + if creds and creds.access_token and not creds.is_expired: + config.providers.anthropic.api_key = creds.access_token + + return config + + def load_config(config_path: Path | None = None) -> Config: """ Load configuration from file or create default. - + Args: config_path: Optional path to config file. Uses default if not provided. - + Returns: Loaded configuration object. """ path = config_path or get_config_path() - + if path.exists(): try: with open(path) as f: data = json.load(f) data = _migrate_config(data) - return Config.model_validate(convert_keys(data)) + config = Config.model_validate(convert_keys(data)) + return _inject_oauth_credentials(config) except (json.JSONDecodeError, ValueError) as e: print(f"Warning: Failed to load config from {path}: {e}") print("Using default configuration.") - - return Config() + + return _inject_oauth_credentials(Config()) def save_config(config: Config, config_path: Path | None = None) -> None: diff --git a/tests/test_config_oauth_integration.py b/tests/test_config_oauth_integration.py new file mode 100644 index 0000000..8ef6f1a --- /dev/null +++ b/tests/test_config_oauth_integration.py @@ -0,0 +1,65 @@ +"""Test OAuth store integration with config loading.""" +import json +import pytest +import tempfile +from pathlib import Path +from nanobot.config.loader import load_config +from nanobot.config.oauth_store import OAuthStore +from nanobot.config.schema import OAuthCredentials + + +def test_oauth_token_injected_into_config(tmp_path, monkeypatch): + """OAuth token from store should be injected into provider api_key.""" + # Create a minimal config file (no api key set) + config_path = tmp_path / "config.json" + config_path.write_text(json.dumps({ + "agents": {"defaults": {"model": "anthropic/claude-opus-4-5"}}, + "providers": {"anthropic": {"apiKey": ""}} + })) + + # Save OAuth credentials + store = OAuthStore(tmp_path) + creds = OAuthCredentials(access_token="sk-ant-oat01-test-inject") + store.save("anthropic", creds) + + # Monkeypatch get_config_path to use our tmp dir + monkeypatch.setattr("nanobot.config.loader.get_config_path", lambda: config_path) + # Monkeypatch the OAuth store path + monkeypatch.setattr("nanobot.config.loader._get_oauth_store_dir", lambda: tmp_path) + + config = load_config(config_path) + + assert config.providers.anthropic.api_key == "sk-ant-oat01-test-inject" + + +def test_config_without_oauth_unchanged(tmp_path, monkeypatch): + """Config without OAuth store should load normally.""" + config_path = tmp_path / "config.json" + config_path.write_text(json.dumps({ + "providers": {"anthropic": {"apiKey": "sk-ant-api03-regular"}} + })) + + monkeypatch.setattr("nanobot.config.loader._get_oauth_store_dir", lambda: tmp_path / "nonexistent") + + config = load_config(config_path) + + assert config.providers.anthropic.api_key == "sk-ant-api03-regular" + + +def test_oauth_does_not_overwrite_existing_key(tmp_path, monkeypatch): + """If user already has an API key, OAuth should still override (OAuth takes priority).""" + config_path = tmp_path / "config.json" + config_path.write_text(json.dumps({ + "providers": {"anthropic": {"apiKey": "sk-ant-api03-existing"}} + })) + + store = OAuthStore(tmp_path) + creds = OAuthCredentials(access_token="sk-ant-oat01-oauth-wins") + store.save("anthropic", creds) + + monkeypatch.setattr("nanobot.config.loader._get_oauth_store_dir", lambda: tmp_path) + + config = load_config(config_path) + + # OAuth token takes priority over existing API key + assert config.providers.anthropic.api_key == "sk-ant-oat01-oauth-wins"