Files
archon/python/src/server/config/config.py
Rasmus Widing 3800280f2e Add Supabase key validation and simplify frontend state management
- Add backend validation to detect and warn about anon vs service keys
- Prevent startup with incorrect Supabase key configuration
- Consolidate frontend state management following KISS principles
- Remove duplicate state tracking and sessionStorage polling
- Add clear error display when backend fails to start
- Improve .env.example documentation with detailed key selection guide
- Add comprehensive test coverage for validation logic
- Remove unused test results checking to eliminate 404 errors

The implementation now warns users about key misconfiguration while
maintaining backward compatibility. Frontend state is simplified with
MainLayout as the single source of truth for backend status.
2025-08-16 00:10:23 +03:00

190 lines
6.5 KiB
Python

"""
Environment configuration management for the MCP server.
"""
import os
from dataclasses import dataclass
from urllib.parse import urlparse
import jwt
class ConfigurationError(Exception):
"""Raised when there's an error in configuration."""
pass
@dataclass
class EnvironmentConfig:
"""Configuration loaded from environment variables."""
supabase_url: str
supabase_service_key: str
port: int # Required - no default
openai_api_key: str | None = None
host: str = "0.0.0.0"
transport: str = "sse"
@dataclass
class RAGStrategyConfig:
"""Configuration for RAG strategies."""
use_contextual_embeddings: bool = False
use_hybrid_search: bool = True
use_agentic_rag: bool = True
use_reranking: bool = True
def validate_openai_api_key(api_key: str) -> bool:
"""Validate OpenAI API key format."""
if not api_key:
raise ConfigurationError("OpenAI API key cannot be empty")
if not api_key.startswith("sk-"):
raise ConfigurationError("OpenAI API key must start with 'sk-'")
return True
def validate_supabase_key(supabase_key: str) -> tuple[bool, str]:
"""Validate Supabase key type and return validation result.
Returns:
tuple[bool, str]: (is_valid, message)
- (False, "ANON_KEY_DETECTED") if anon key detected
- (True, "VALID_SERVICE_KEY") if service key detected
- (False, "UNKNOWN_KEY_TYPE:{role}") for unknown roles
- (True, "UNABLE_TO_VALIDATE") if JWT cannot be decoded
"""
if not supabase_key:
return False, "EMPTY_KEY"
try:
# Decode JWT without verification to check the 'role' claim
# We don't verify the signature since we only need to check the role
decoded = jwt.decode(supabase_key, options={"verify_signature": False})
role = decoded.get("role")
if role == "anon":
return False, "ANON_KEY_DETECTED"
elif role == "service_role":
return True, "VALID_SERVICE_KEY"
else:
return False, f"UNKNOWN_KEY_TYPE:{role}"
except Exception:
# If we can't decode the JWT, we'll allow it to proceed
# This handles new key formats or non-JWT keys
return True, "UNABLE_TO_VALIDATE"
def validate_supabase_url(url: str) -> bool:
"""Validate Supabase URL format."""
if not url:
raise ConfigurationError("Supabase URL cannot be empty")
parsed = urlparse(url)
if parsed.scheme != "https":
raise ConfigurationError("Supabase URL must use HTTPS")
if not parsed.netloc:
raise ConfigurationError("Invalid Supabase URL format")
return True
def load_environment_config() -> EnvironmentConfig:
"""Load and validate environment configuration."""
# OpenAI API key is optional at startup - can be set via API
openai_api_key = os.getenv("OPENAI_API_KEY")
# Required environment variables for database access
supabase_url = os.getenv("SUPABASE_URL")
if not supabase_url:
raise ConfigurationError("SUPABASE_URL environment variable is required")
supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY")
if not supabase_service_key:
raise ConfigurationError("SUPABASE_SERVICE_KEY environment variable is required")
# Validate required fields
if openai_api_key:
validate_openai_api_key(openai_api_key)
validate_supabase_url(supabase_url)
# Validate Supabase key type
is_valid_key, key_message = validate_supabase_key(supabase_service_key)
if not is_valid_key:
if key_message == "ANON_KEY_DETECTED":
raise ConfigurationError(
"CRITICAL: You are using a Supabase ANON key instead of a SERVICE key.\n\n"
"The ANON key is a public key with read-only permissions that cannot write to the database.\n"
"This will cause all database operations to fail with 'permission denied' errors.\n\n"
"To fix this:\n"
"1. Go to your Supabase project dashboard\n"
"2. Navigate to Settings > API keys\n"
"3. Find the 'service_role' key (NOT the 'anon' key)\n"
"4. Update your SUPABASE_SERVICE_KEY environment variable\n\n"
"Key characteristics:\n"
"- ANON key: Starts with 'eyJ...' and has role='anon' (public, read-only)\n"
"- SERVICE key: Starts with 'eyJ...' and has role='service_role' (private, full access)\n\n"
"Current key role detected: anon"
)
elif key_message.startswith("UNKNOWN_KEY_TYPE:"):
role = key_message.split(":", 1)[1]
print(f"WARNING: Unknown Supabase key role '{role}'. Proceeding but may cause issues.")
# For UNABLE_TO_VALIDATE, we continue silently
# Optional environment variables with defaults
host = os.getenv("HOST", "0.0.0.0")
port_str = os.getenv("PORT")
if not port_str:
# This appears to be for MCP configuration based on default 8051
port_str = os.getenv("ARCHON_MCP_PORT")
if not port_str:
raise ConfigurationError(
"PORT or ARCHON_MCP_PORT environment variable is required. "
"Please set it in your .env file or environment. "
"Default value: 8051"
)
transport = os.getenv("TRANSPORT", "sse")
# Validate and convert port
try:
port = int(port_str)
except ValueError as e:
raise ConfigurationError(f"PORT must be a valid integer, got: {port_str}") from e
return EnvironmentConfig(
openai_api_key=openai_api_key,
supabase_url=supabase_url,
supabase_service_key=supabase_service_key,
host=host,
port=port,
transport=transport,
)
def get_config() -> EnvironmentConfig:
"""Get environment configuration with validation."""
return load_environment_config()
def get_rag_strategy_config() -> RAGStrategyConfig:
"""Load RAG strategy configuration from environment variables."""
def str_to_bool(value: str | None) -> bool:
"""Convert string environment variable to boolean."""
if value is None:
return False
return value.lower() in ("true", "1", "yes", "on")
return RAGStrategyConfig(
use_contextual_embeddings=str_to_bool(os.getenv("USE_CONTEXTUAL_EMBEDDINGS")),
use_hybrid_search=str_to_bool(os.getenv("USE_HYBRID_SEARCH")),
use_agentic_rag=str_to_bool(os.getenv("USE_AGENTIC_RAG")),
use_reranking=str_to_bool(os.getenv("USE_RERANKING")),
)