modul-x / app /config.py
Z User
v3.1.0: Part B architecture migration — eliminate Hugging Face inference credit dependency
6c516b6
Raw History Blame Contribute Delete
8.45 kB
from urllib.parse import urlsplit
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List
def _is_https_origin(value: str) -> bool:
try:
parsed = urlsplit(value)
_ = parsed.port
except ValueError:
return False
return (
parsed.scheme == "https"
and bool(parsed.hostname)
and parsed.username is None
and parsed.password is None
and parsed.path in {"", "/"}
and not parsed.query
and not parsed.fragment
)
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# App
app_environment: str = "development"
app_name: str = "BRIDGE Modul - X"
app_version: str = "3.1.0"
debug: bool = False
secret_key: str = "change-me-in-production"
# Server
host: str = "0.0.0.0"
port: int = 8000
workers: int = 4
# Database
database_url: str = "sqlite+aiosqlite:///./emailval.db"
# Redis
redis_url: str = "redis://localhost:6379/0"
redis_cache_ttl: int = 86400 # 24 hours
# CORS
cors_origins: List[str] = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
]
# Validation
smtp_timeout: int = 10
dns_timeout: int = 5
# Public resolvers used instead of the host's own resolver config. A stale
# gateway list (dead hotspot IPs) makes every lookup burn the full timeout
# and fail — see dns_check.build_resolver.
dns_nameservers: list[str] = ["1.1.1.1", "8.8.8.8"]
# Bulk validation only routes through Celery on hosts that actually run a
# worker. The HF Space runs uvicorn alone (Dockerfile CMD), so a reachable
# Redis must not by itself select the Celery branch — jobs would sit in
# the queue forever. Opt in on the VPS deployment.
celery_bulk_enabled: bool = False
max_bulk_emails: int = 10000
rate_limit_per_minute: int = 60
bulk_sync_chunk_size: int = 50 # Process this many emails at a time in sync mode
# Proxy
use_proxies: bool = False
proxy_list: List[str] = []
# Mailbox verification provider (Layer 5 catch-all + Layer 6 SMTP).
# The engine is FULLY AUTONOMOUS: mailbox verdicts come from its own SMTP
# probing engine — never from a third-party verification API. "local"
# forces the built-in engine; "remote" forces the remote SMTP worker;
# "auto" prefers the remote worker when SMTP_WORKER_URL is set and falls
# back to the built-in engine otherwise.
mailbox_provider: str = "auto" # auto | local | remote
# Public HELO/MX identity used in the SMTP dialogue. A real, resolvable
# hostname with matching PTR improves acceptance by strict receivers.
smtp_helo_domain: str = "validator.local"
smtp_mail_from: str = "probe@validator.local"
# Remote SMTP probe worker (spec §3.3): orchestrator and SMTP worker are
# separate deployments. Point this at a port-25-capable host running this
# same image (its POST /api/v1/smtp/probe endpoints) and every mailbox
# verdict flows through it. Shared secret via SMTP_WORKER_TOKEN.
smtp_worker_url: str = ""
smtp_worker_token: str = ""
smtp_worker_timeout: int = 15
# Circuit breaker: after this many consecutive direct-SMTP connection
# failures (blocked egress), fail fast for `smtp_cooldown_seconds` instead
# of burning the full timeout on every address.
smtp_fail_threshold: int = 3
smtp_cooldown_seconds: int = 300
# Catch-all is a domain property — cache the verdict for 72h so a bulk list
# does not re-probe the same domain for every address.
catch_all_ttl_seconds: int = 72 * 3600
# DNS/MX answers change slowly; a definitive mailbox verdict is stable for
# about a day. Transient failures are never cached regardless of TTL.
dns_cache_ttl_seconds: int = 24 * 3600
# Per-MX-host SMTP session cap and global concurrency for bulk runs.
max_connections_per_host: int = 2
bulk_concurrency: int = 20
# --- DNS-over-HTTPS worker (spec §3.2) ---
# MX resolution over free public DoH resolvers (Google + Cloudflare) with a
# dual-resolver consistency check. Legacy dnspython stays as fallback.
doh_enabled: bool = True
doh_timeout: int = 5
# --- AI Risk Scorer (spec §3.4 — LOCAL inference, zero external credits) ---
# Part B architecture migration: the Hugging Face Inference API path was
# REMOVED. The model id below names a public HF Hub checkpoint whose
# weights are pulled once (free, unmetered) and then run in-process via a
# transformers pipeline — no token, no inference credits, no remote calls.
# A DistilBERT-class NLI checkpoint is the spec-default (§5.3): same
# feature set, ~4x lower CPU latency than bart-large-mnli on 2 vCPU.
# Empty disables inference entirely (the rule-based heuristic then scores
# every ambiguous address at zero marginal cost). Configured, never
# hard-coded.
ai_scorer_model: str = "typeform/distilbert-base-uncased-mnli"
ai_scorer_timeout: int = 12
# Zero-shot mode: candidate labels ARE the risk taxonomy — works with any
# local NLI checkpoint (e.g. typeform/distilbert-base-uncased-mnli or
# facebook/bart-large-mnli if CPU budget allows).
ai_scorer_zero_shot: bool = True
# Master switch: False forces heuristic-only scoring (no model load at all).
ai_scorer_local: bool = True
# §2.2 cache aggressively: a repeated PII-free feature string never
# re-invokes the pipeline inside this TTL window. 0 disables the cache.
ai_score_cache_ttl_seconds: int = 3600
ai_circuit_fail_threshold: int = 3
ai_circuit_cooldown_seconds: int = 300
# --- DistilBERT email-risk signal (advisory, unknown domains only) ---
# OFF by default: with the flag off the engine's behaviour is identical to
# the pre-ML build. The model is a public HF repo trained on the engine's
# own blocklists (see app/ml/). Promotion from advisory to blocking requires
# measured DISPOSABLE precision >= 0.98 (app/ml/train.py reports it).
# Hub downloads are free and unmetered; no token is required or used.
ml_disposable_enabled: bool = False
ml_model_repo: str = ""
ml_batch_size: int = 64
ml_score_penalty: int = 5
# Run-quality monitor (report item 10): alert when >= this share of a bulk
# run comes back risky/unknown/error — the signature of a systemic failure
# (dead provider, blocked egress, engine regression). 0 disables it.
risky_rate_alert_threshold: float = 0.5
# Celery
celery_broker_url: str = "redis://localhost:6379/1"
celery_result_backend: str = "redis://localhost:6379/2"
# Supabase (Postgres + PostgREST)
supabase_url: str = ""
supabase_anon_key: str = ""
supabase_service_role_key: str = ""
enable_db_persistence: bool = True
# Central billing gate: "true" enforces Supabase JWT + active license
# (modul-x directly, via combo-validate-send, or via combo-full-bridge)
# on every paid API route. Anything else = open demo mode.
license_enforcement: str = ""
@model_validator(mode="after")
def validate_production_settings(self) -> "Settings":
if self.app_environment.strip().casefold() != "production":
return self
problems = []
if self.debug:
problems.append("DEBUG must be false")
if self.license_enforcement.strip().casefold() != "true":
problems.append("LICENSE_ENFORCEMENT must be true")
if not self.supabase_configured:
problems.append("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required")
elif not _is_https_origin(self.supabase_url):
problems.append("SUPABASE_URL must be an HTTPS URL")
if not self.cors_origins or not all(
_is_https_origin(origin) for origin in self.cors_origins
):
problems.append("CORS_ORIGINS must contain only HTTPS origins")
if problems:
raise ValueError("Unsafe production configuration: " + "; ".join(problems))
return self
@property
def supabase_configured(self) -> bool:
return bool(self.supabase_url and self.supabase_service_role_key)
settings = Settings()