feat(config): integrate OAuth store with config loading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,35 +12,53 @@ def get_config_path() -> Path:
|
|||||||
return Path.home() / ".nanobot" / "config.json"
|
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:
|
def get_data_dir() -> Path:
|
||||||
"""Get the nanobot data directory."""
|
"""Get the nanobot data directory."""
|
||||||
from nanobot.utils.helpers import get_data_path
|
from nanobot.utils.helpers import get_data_path
|
||||||
return 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:
|
def load_config(config_path: Path | None = None) -> Config:
|
||||||
"""
|
"""
|
||||||
Load configuration from file or create default.
|
Load configuration from file or create default.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path: Optional path to config file. Uses default if not provided.
|
config_path: Optional path to config file. Uses default if not provided.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Loaded configuration object.
|
Loaded configuration object.
|
||||||
"""
|
"""
|
||||||
path = config_path or get_config_path()
|
path = config_path or get_config_path()
|
||||||
|
|
||||||
if path.exists():
|
if path.exists():
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
data = _migrate_config(data)
|
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:
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
print(f"Warning: Failed to load config from {path}: {e}")
|
print(f"Warning: Failed to load config from {path}: {e}")
|
||||||
print("Using default configuration.")
|
print("Using default configuration.")
|
||||||
|
|
||||||
return Config()
|
return _inject_oauth_credentials(Config())
|
||||||
|
|
||||||
|
|
||||||
def save_config(config: Config, config_path: Path | None = None) -> None:
|
def save_config(config: Config, config_path: Path | None = None) -> None:
|
||||||
|
|||||||
65
tests/test_config_oauth_integration.py
Normal file
65
tests/test_config_oauth_integration.py
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user