diff --git a/codex/cli.py b/codex/cli.py index 4ba4152..3acac41 100644 --- a/codex/cli.py +++ b/codex/cli.py @@ -45,7 +45,7 @@ def migrate() -> None: from codex.db import apply_schema settings = get_settings() - url = settings.migration_database_url or settings.database_url + url = (settings.migration_database_url or settings.database_url).get_secret_value() try: with psycopg.connect(url, row_factory=psycopg.rows.dict_row) as conn: apply_schema(conn) diff --git a/codex/config.py b/codex/config.py index faf79ad..0adac76 100644 --- a/codex/config.py +++ b/codex/config.py @@ -9,7 +9,7 @@ from __future__ import annotations from functools import lru_cache -from pydantic import AliasChoices, Field +from pydantic import AliasChoices, Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict @@ -26,15 +26,15 @@ class Settings(BaseSettings): # ------------------------------------------------------------------ # Database # ------------------------------------------------------------------ - database_url: str = Field( - default="postgresql://researcher:change_me@localhost:5432/papers", + database_url: SecretStr = Field( + default=SecretStr("postgresql://researcher:change_me@localhost:5432/papers"), description=( "libpq-compatible connection string consumed by psycopg. " "Example: postgresql://user:pass@host:5432/dbname" ), ) - migration_database_url: str | None = Field( + migration_database_url: SecretStr | None = Field( default=None, description=( "Optional privileged connection string used by `codex migrate` to apply " @@ -148,7 +148,7 @@ class Settings(BaseSettings): # ------------------------------------------------------------------ # F-09 Rich Parsing # ------------------------------------------------------------------ - mathpix_app_id: str | None = Field( + mathpix_app_id: SecretStr | None = Field( default=None, description=( "MathPix App ID for cloud formula extraction. " @@ -156,7 +156,7 @@ class Settings(BaseSettings): ), ) - mathpix_app_key: str | None = Field( + mathpix_app_key: SecretStr | None = Field( default=None, description=( "MathPix App Key for cloud formula extraction. " @@ -257,7 +257,7 @@ class Settings(BaseSettings): description="Bind port for HTTP transport (ignored for stdio).", ) - mcp_auth_token: str | None = Field( + mcp_auth_token: SecretStr | None = Field( default=None, description=( "Bearer token required when mcp_transport='http'. " diff --git a/codex/db.py b/codex/db.py index f8272b9..384d4ae 100644 --- a/codex/db.py +++ b/codex/db.py @@ -36,7 +36,7 @@ def get_conn() -> Generator[psycopg.Connection[psycopg.rows.DictRow], None, None """ settings = get_settings() with psycopg.connect( - settings.database_url, + settings.database_url.get_secret_value(), row_factory=psycopg.rows.dict_row, ) as conn: yield conn diff --git a/codex/mcp_server.py b/codex/mcp_server.py index 5a82b08..9991627 100644 --- a/codex/mcp_server.py +++ b/codex/mcp_server.py @@ -294,12 +294,13 @@ def _require_http_token() -> str: from codex.config import get_settings settings = get_settings() - if not settings.mcp_auth_token: + token = settings.mcp_auth_token.get_secret_value() if settings.mcp_auth_token else "" + if not token: raise RuntimeError( "HTTP transport requires MCP_AUTH_TOKEN to be set. " "Set the environment variable or add it to .env." ) - return settings.mcp_auth_token + return token # --------------------------------------------------------------------------- diff --git a/codex/parsing/mathpix.py b/codex/parsing/mathpix.py index 1e4b2f8..f598997 100644 --- a/codex/parsing/mathpix.py +++ b/codex/parsing/mathpix.py @@ -302,8 +302,10 @@ def extract_formulas( """ settings = get_settings() - app_id = mathpix_app_id or settings.mathpix_app_id or "" - app_key = mathpix_app_key or settings.mathpix_app_key or "" + settings_id = settings.mathpix_app_id.get_secret_value() if settings.mathpix_app_id else "" + settings_key = settings.mathpix_app_key.get_secret_value() if settings.mathpix_app_key else "" + app_id = mathpix_app_id or settings_id + app_key = mathpix_app_key or settings_key if app_id and app_key: logger.info("Using MathPix backend for %s", pdf_path) diff --git a/tests/mcp/test_http_auth.py b/tests/mcp/test_http_auth.py index 74c8c79..ceed096 100644 --- a/tests/mcp/test_http_auth.py +++ b/tests/mcp/test_http_auth.py @@ -12,6 +12,7 @@ from __future__ import annotations from unittest.mock import MagicMock, patch import pytest +from pydantic import SecretStr # --------------------------------------------------------------------------- # _require_http_token @@ -37,7 +38,7 @@ def test_require_http_token_returns_token_when_set() -> None: from codex.mcp_server import _require_http_token mock_settings = MagicMock() - mock_settings.mcp_auth_token = "super-secret-token" + mock_settings.mcp_auth_token = SecretStr("super-secret-token") with patch("codex.config.get_settings", return_value=mock_settings): result = _require_http_token() @@ -97,7 +98,7 @@ def test_main_http_with_token_calls_uvicorn() -> None: mock_settings = MagicMock() mock_settings.mcp_transport = "http" - mock_settings.mcp_auth_token = "test-token" + mock_settings.mcp_auth_token = SecretStr("test-token") mock_settings.mcp_host = "127.0.0.1" mock_settings.mcp_port = 8765 diff --git a/tests/parsing/test_mathpix.py b/tests/parsing/test_mathpix.py index ce3597c..03d52ee 100644 --- a/tests/parsing/test_mathpix.py +++ b/tests/parsing/test_mathpix.py @@ -10,6 +10,7 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest +from pydantic import SecretStr from codex.models import FormulaChunk @@ -295,8 +296,8 @@ class TestRouteSelection: def test_mathpix_selected_when_creds_present(self) -> None: """extract_formulas calls MathPix backend when both creds are set.""" mock_settings = MagicMock() - mock_settings.mathpix_app_id = "my_id" - mock_settings.mathpix_app_key = "my_key" + mock_settings.mathpix_app_id = SecretStr("my_id") + mock_settings.mathpix_app_key = SecretStr("my_key") mock_settings.pix2tex_fallback = True with ( diff --git a/tests/scaffold/test_config.py b/tests/scaffold/test_config.py index f8bde0c..c11751d 100644 --- a/tests/scaffold/test_config.py +++ b/tests/scaffold/test_config.py @@ -8,7 +8,10 @@ from codex.config import Settings, get_settings def test_settings_defaults() -> None: """Settings() is constructable without env vars; expected defaults are set.""" s = Settings() - assert s.database_url == "postgresql://researcher:change_me@localhost:5432/papers" + assert ( + s.database_url.get_secret_value() + == "postgresql://researcher:change_me@localhost:5432/papers" + ) assert s.embedding_model == "BAAI/bge-m3" assert s.embedding_dim == 1024 @@ -20,7 +23,7 @@ def test_settings_env_override(monkeypatch: object) -> None: mp: pytest.MonkeyPatch = monkeypatch # type: ignore[assignment] mp.setenv("DATABASE_URL", "postgresql://x:y@h:5432/db") s = Settings() - assert s.database_url == "postgresql://x:y@h:5432/db" + assert s.database_url.get_secret_value() == "postgresql://x:y@h:5432/db" def test_get_settings_is_singleton() -> None: