strat-chatbot/config/settings.py
Cal Corum 43d36ce439 fix: resolve HIGH-severity issues from code review
API authentication:
- Add X-API-Secret shared-secret header validation on /chat and /stats
- /health remains public for monitoring
- Auth is a no-op when API_SECRET is empty (dev mode)

Rate limiting:
- Add per-user sliding-window rate limiter on /chat (10 req/60s default)
- Returns 429 with clear message when exceeded
- Self-cleaning memory (prunes expired entries on each check)

Exception sanitization:
- Discord bot no longer exposes raw exception text to users
- Error embeds show generic "Something went wrong" message
- Full exception details logged server-side with context
- query_chat_api RuntimeError no longer includes response body

Async correctness:
- Wrap synchronous RuleRepository.search() in run_in_executor()
  to prevent blocking the event loop during SentenceTransformer inference
- Port contract stays synchronous; service owns the async boundary

Test coverage: 101 passed, 1 skipped (11 new tests for auth + rate limiting)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:00:26 -05:00

80 lines
3.4 KiB
Python

"""Application settings — Pydantic v2 style, no module-level singleton.
The container (config/container.py) instantiates Settings once at startup
and passes it down to adapters. This keeps tests free of singleton state.
"""
from pathlib import Path
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""All runtime configuration, sourced from environment variables or a .env file.
Fields use explicit ``env=`` aliases so the variable names are immediately
visible and grep-able without needing to know Pydantic's casing rules.
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
# Allow unknown env vars — avoids breakage when the .env file has
# variables that belong to other services (Discord bot, scripts, etc.).
extra="ignore",
)
# ------------------------------------------------------------------
# OpenRouter / LLM
# ------------------------------------------------------------------
openrouter_api_key: str = Field(default="", alias="OPENROUTER_API_KEY")
openrouter_model: str = Field(
default="stepfun/step-3.5-flash:free", alias="OPENROUTER_MODEL"
)
# ------------------------------------------------------------------
# Discord
# ------------------------------------------------------------------
discord_bot_token: str = Field(default="", alias="DISCORD_BOT_TOKEN")
discord_guild_id: Optional[str] = Field(default=None, alias="DISCORD_GUILD_ID")
# ------------------------------------------------------------------
# Gitea issue tracker
# ------------------------------------------------------------------
gitea_token: str = Field(default="", alias="GITEA_TOKEN")
gitea_owner: str = Field(default="cal", alias="GITEA_OWNER")
gitea_repo: str = Field(default="strat-chatbot", alias="GITEA_REPO")
gitea_base_url: str = Field(
default="https://git.manticorum.com/api/v1", alias="GITEA_BASE_URL"
)
# ------------------------------------------------------------------
# File-system paths
# ------------------------------------------------------------------
data_dir: Path = Field(default=Path("./data"), alias="DATA_DIR")
rules_dir: Path = Field(default=Path("./data/rules"), alias="RULES_DIR")
chroma_dir: Path = Field(default=Path("./data/chroma"), alias="CHROMA_DIR")
# ------------------------------------------------------------------
# Database
# ------------------------------------------------------------------
db_url: str = Field(
default="sqlite+aiosqlite:///./data/conversations.db", alias="DB_URL"
)
# ------------------------------------------------------------------
# API authentication
# ------------------------------------------------------------------
api_secret: str = Field(default="", alias="API_SECRET")
# ------------------------------------------------------------------
# Conversation / retrieval tuning
# ------------------------------------------------------------------
conversation_ttl: int = Field(default=1800, alias="CONVERSATION_TTL")
top_k_rules: int = Field(default=10, alias="TOP_K_RULES")
embedding_model: str = Field(
default="sentence-transformers/all-MiniLM-L6-v2", alias="EMBEDDING_MODEL"
)