feat(07-01): services/ai_config.py HKDF helpers, ProviderConfig loader, env seed

- Create backend/services/ai_config.py with:
  - _derive_ai_settings_key(master_key, provider_id): fresh HKDF per call, salt=provider_id.encode("utf-8"), info=b"ai-provider-settings" (domain-separated from b"cloud-credentials")
  - encrypt_api_key / decrypt_api_key: Fernet round-trip without JSON wrapping
  - load_provider_config(session): reads is_active=True row from system_settings, decrypts api_key_enc; returns stub ProviderConfig (real class lands in Plan 02)
  - seed_system_settings_from_env(session): idempotent insert of default provider on first boot
  - _ProviderConfigStub: minimal Pydantic model stub until ai/provider_config.py exists (Plan 02)
- Update backend/main.py: import seed_system_settings_from_env, call in lifespan with try/except so missing table (pre-migration fresh boot) doesn't crash startup
- Round-trip smoke test: encrypt_api_key → decrypt_api_key == original plaintext
This commit is contained in:
curo1305
2026-06-04 18:50:01 +02:00
parent 4eb317749f
commit 0fd6930b41
2 changed files with 239 additions and 0 deletions
+14
View File
@@ -22,6 +22,7 @@ from api.documents import router as documents_router
from api.topics import router as topics_router
from config import settings
from db.session import AsyncSessionLocal, engine
from services.ai_config import seed_system_settings_from_env
from services.logging import setup_logging
from services.rate_limiting import account_limiter
@@ -158,6 +159,19 @@ async def lifespan(app: FastAPI):
async with AsyncSessionLocal() as session:
await bootstrap_admin(session)
# AI provider seed (D-04 / Phase 7): populate system_settings from env vars
# on first boot. Wrapped in try/except so that a missing table (fresh container
# before migrations run) does not crash startup — logs a warning and skips.
try:
async with AsyncSessionLocal() as session:
await seed_system_settings_from_env(session)
await session.commit()
except Exception as _seed_exc:
import logging as _logging
_logging.getLogger(__name__).warning(
"AI provider seed skipped (table may not exist yet): %s", _seed_exc
)
yield
# Shutdown: close pooled connections and Redis