|
11 | 11 |
|
12 | 12 | from pydantic import Field, field_validator |
13 | 13 | from pydantic_settings import BaseSettings, SettingsConfigDict |
| 14 | +from pydantic_settings.sources import DotEnvSettingsSource |
14 | 15 |
|
15 | 16 | # API key validation patterns and lengths |
16 | 17 | API_KEY_MIN_LENGTH = 20 # Minimum reasonable API key length |
@@ -169,15 +170,49 @@ def add_to_input_history(input_text: str, max_items: int = 100) -> None: |
169 | 170 | _write_secure_file(INPUT_HISTORY_CACHE, json.dumps({"history": history})) |
170 | 171 |
|
171 | 172 |
|
| 173 | +def _get_active_env_file() -> Path: |
| 174 | + """Get the active .env file using local-first priority. |
| 175 | +
|
| 176 | + If ./.env exists in current directory, use it exclusively. |
| 177 | + Otherwise fall back to ~/.quorum/.env for global config. |
| 178 | + """ |
| 179 | + local_env = Path(".env") |
| 180 | + if local_env.exists(): |
| 181 | + return local_env |
| 182 | + return CACHE_DIR / ".env" |
| 183 | + |
| 184 | + |
172 | 185 | class Settings(BaseSettings): |
173 | 186 | """Application settings loaded from environment variables.""" |
174 | 187 |
|
175 | 188 | model_config = SettingsConfigDict( |
176 | | - env_file=(CACHE_DIR / ".env", ".env"), # ~/.quorum/.env first, then ./.env |
177 | 189 | env_file_encoding="utf-8", |
178 | 190 | extra="ignore", |
179 | 191 | ) |
180 | 192 |
|
| 193 | + @classmethod |
| 194 | + def settings_customise_sources( |
| 195 | + cls, |
| 196 | + settings_cls: type[BaseSettings], |
| 197 | + init_settings, |
| 198 | + env_settings, |
| 199 | + dotenv_settings, |
| 200 | + file_secret_settings, |
| 201 | + ): |
| 202 | + """Use local .env if exists, otherwise global ~/.quorum/.env. |
| 203 | +
|
| 204 | + Local-first priority: If ./.env exists in the current working directory, |
| 205 | + use it exclusively. Otherwise fall back to ~/.quorum/.env for global config. |
| 206 | + This prevents confusing merged configs when running from different directories. |
| 207 | + """ |
| 208 | + env_file = _get_active_env_file() |
| 209 | + dotenv_settings = DotEnvSettingsSource( |
| 210 | + settings_cls, |
| 211 | + env_file=env_file, |
| 212 | + env_file_encoding="utf-8", |
| 213 | + ) |
| 214 | + return (init_settings, env_settings, dotenv_settings, file_secret_settings) |
| 215 | + |
181 | 216 | # API Keys |
182 | 217 | openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY") |
183 | 218 | anthropic_api_key: str | None = Field(default=None, alias="ANTHROPIC_API_KEY") |
|
0 commit comments