Files
openclaw-mission-control/backend/app/core/config.py
Hugh Brown 8e145a2129 security: set sensible defaults for security response headers
X-Content-Type-Options, X-Frame-Options, and Referrer-Policy all
defaulted to empty (disabled). Set defaults to nosniff, DENY, and
strict-origin-when-cross-origin respectively. Operators can still
override or disable via environment variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:35:10 +05:30

115 lines
3.9 KiB
Python

"""Application settings and environment configuration loading."""
from __future__ import annotations
from pathlib import Path
from typing import Self
from urllib.parse import urlparse
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.core.auth_mode import AuthMode
BACKEND_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_ENV_FILE = BACKEND_ROOT / ".env"
LOCAL_AUTH_TOKEN_MIN_LENGTH = 50
LOCAL_AUTH_TOKEN_PLACEHOLDERS = frozenset(
{
"change-me",
"changeme",
"replace-me",
"replace-with-strong-random-token",
},
)
class Settings(BaseSettings):
"""Typed runtime configuration sourced from environment variables."""
model_config = SettingsConfigDict(
# Load `backend/.env` regardless of current working directory.
# (Important when running uvicorn from repo root or via a process manager.)
env_file=[DEFAULT_ENV_FILE, ".env"],
env_file_encoding="utf-8",
extra="ignore",
)
environment: str = "dev"
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/openclaw_agency"
# Auth mode: "clerk" for Clerk JWT auth, "local" for shared bearer token auth.
auth_mode: AuthMode
local_auth_token: str = ""
# Clerk auth (auth only; roles stored in DB)
clerk_secret_key: str = ""
clerk_api_url: str = "https://api.clerk.com"
clerk_verify_iat: bool = True
clerk_leeway: float = 10.0
cors_origins: str = ""
base_url: str = ""
# Security response headers (set to blank to disable a specific header)
security_header_x_content_type_options: str = "nosniff"
security_header_x_frame_options: str = "DENY"
security_header_referrer_policy: str = "strict-origin-when-cross-origin"
security_header_permissions_policy: str = ""
# Database lifecycle
db_auto_migrate: bool = False
# RQ queueing / dispatch
rq_redis_url: str = "redis://localhost:6379/0"
rq_queue_name: str = "default"
rq_dispatch_throttle_seconds: float = 15.0
rq_dispatch_max_retries: int = 3
rq_dispatch_retry_base_seconds: float = 10.0
rq_dispatch_retry_max_seconds: float = 120.0
# OpenClaw gateway runtime compatibility
gateway_min_version: str = "2026.02.9"
# Logging
log_level: str = "INFO"
log_format: str = "text"
log_use_utc: bool = False
request_log_slow_ms: int = Field(default=1000, ge=0)
request_log_include_health: bool = False
@model_validator(mode="after")
def _defaults(self) -> Self:
if self.auth_mode == AuthMode.CLERK:
if not self.clerk_secret_key.strip():
raise ValueError(
"CLERK_SECRET_KEY must be set and non-empty when AUTH_MODE=clerk.",
)
elif self.auth_mode == AuthMode.LOCAL:
token = self.local_auth_token.strip()
if (
not token
or len(token) < LOCAL_AUTH_TOKEN_MIN_LENGTH
or token.lower() in LOCAL_AUTH_TOKEN_PLACEHOLDERS
):
raise ValueError(
"LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local.",
)
base_url = self.base_url.strip()
if not base_url:
raise ValueError("BASE_URL must be set and non-empty.")
parsed_base_url = urlparse(base_url)
if parsed_base_url.scheme not in {"http", "https"} or not parsed_base_url.netloc:
raise ValueError(
"BASE_URL must be an absolute http(s) URL (e.g. http://localhost:8000).",
)
self.base_url = base_url.rstrip("/")
# In dev, default to applying Alembic migrations at startup to avoid
# schema drift (e.g. missing newly-added columns).
if "db_auto_migrate" not in self.model_fields_set and self.environment == "dev":
self.db_auto_migrate = True
return self
settings = Settings()