From 2e7f3d1b29221b493d2158e54e58ebc0a88bec5c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 18:19:22 +0000 Subject: [PATCH] feat: centralize L402 config, automate Metal install, fix watchdog cleanup - config.py: add L402_HMAC_SECRET, L402_MACAROON_SECRET, LIGHTNING_BACKEND to pydantic-settings with startup warnings for default secrets - l402_proxy.py, mock_backend.py, factory.py: migrate from os.environ.get() to `from config import settings` per project convention - Makefile: `make install-creative` now auto-installs PyTorch nightly with Metal (MPS) support on Apple Silicon instead of just printing a note - activate_self_tdd.sh: add PID file (.watchdog.pid) and EXIT trap so Ctrl-C cleanly stops both the dashboard and the watchdog process - .gitignore: add .watchdog.pid https://claude.ai/code/session_01A81E5HMxZEPxzv2acNo35u --- .gitignore | 3 +++ Makefile | 14 +++++++------- activate_self_tdd.sh | 26 ++++++++++++++++++++++++-- src/config.py | 25 +++++++++++++++++++++++++ src/lightning/factory.py | 7 ++++--- src/lightning/mock_backend.py | 13 +++---------- src/timmy_serve/l402_proxy.py | 17 ++++------------- 7 files changed, 70 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 45c6e09..4423510 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ env/ # SQLite memory — never commit agent memory *.db +# Runtime PID files +.watchdog.pid + # Chat platform state files (contain bot tokens) telegram_state.json discord_state.json diff --git a/Makefile b/Makefile index 498220e..3ef4a46 100644 --- a/Makefile +++ b/Makefile @@ -27,14 +27,14 @@ install-bigbrain: $(VENV)/bin/activate install-creative: $(VENV)/bin/activate $(PIP) install --quiet -e ".[dev,creative]" - @if [ "$$(uname -s)" = "Darwin" ]; then \ - echo ""; \ - echo " Note: PyTorch on macOS uses CPU by default."; \ - echo " For Metal (GPU) acceleration, install the nightly build:"; \ - echo " pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu"; \ - echo ""; \ + @if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname -s)" = "Darwin" ]; then \ + echo " Apple Silicon detected — installing PyTorch with Metal (MPS) support..."; \ + $(PIP) install --quiet --pre torch torchvision torchaudio \ + --index-url https://download.pytorch.org/whl/nightly/cpu; \ + echo "✓ Creative extras installed with Metal GPU acceleration"; \ + else \ + echo "✓ Creative extras installed (diffusers, torch, ace-step)"; \ fi - @echo "✓ Creative extras installed (diffusers, torch, ace-step)" $(VENV)/bin/activate: python3 -m venv $(VENV) diff --git a/activate_self_tdd.sh b/activate_self_tdd.sh index 1268f77..0d89e03 100755 --- a/activate_self_tdd.sh +++ b/activate_self_tdd.sh @@ -60,15 +60,37 @@ python -m pytest "$REPO_DIR/tests/" -q --tb=short echo "==> All tests passed." # ── 4. Self-TDD watchdog (background) ──────────────────────────────────────── +WATCHDOG_PID_FILE="$REPO_DIR/.watchdog.pid" + +# Kill any previously orphaned watchdog +if [[ -f "$WATCHDOG_PID_FILE" ]]; then + OLD_PID=$(cat "$WATCHDOG_PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "==> Stopping previous watchdog (PID $OLD_PID)..." + kill "$OLD_PID" 2>/dev/null || true + fi + rm -f "$WATCHDOG_PID_FILE" +fi + echo "==> Starting self-TDD watchdog (60s interval) in background..." self-tdd watch --interval 60 & WATCHDOG_PID=$! -echo " Watchdog PID: $WATCHDOG_PID" +echo "$WATCHDOG_PID" > "$WATCHDOG_PID_FILE" +echo " Watchdog PID: $WATCHDOG_PID (saved to .watchdog.pid)" echo " Kill with: kill $WATCHDOG_PID" +# Clean up watchdog when the script exits (Ctrl-C, etc.) +cleanup() { + echo "" + echo "==> Stopping watchdog (PID $WATCHDOG_PID)..." + kill "$WATCHDOG_PID" 2>/dev/null || true + rm -f "$WATCHDOG_PID_FILE" +} +trap cleanup EXIT + # ── 5. Dashboard ───────────────────────────────────────────────────────────── echo "" echo "==> Starting Timmy Time dashboard at http://localhost:8000" -echo " Ctrl-C stops the dashboard (watchdog continues until you kill it)" +echo " Ctrl-C stops both the dashboard and the watchdog" echo "" uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 diff --git a/src/config.py b/src/config.py index f90606f..d35f3cc 100644 --- a/src/config.py +++ b/src/config.py @@ -59,6 +59,14 @@ class Settings(BaseSettings): video_transition_duration: float = 1.0 default_video_codec: str = "libx264" + # ── L402 Lightning ─────────────────────────────────────────────────── + # HMAC secrets for macaroon signing and invoice verification. + # MUST be changed from defaults before deploying to production. + # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" + l402_hmac_secret: str = "timmy-hmac-secret" + l402_macaroon_secret: str = "timmy-macaroon-secret" + lightning_backend: Literal["mock", "lnd"] = "mock" + model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", @@ -67,3 +75,20 @@ class Settings(BaseSettings): settings = Settings() + +# ── Startup validation ─────────────────────────────────────────────────────── +# Warn when security-sensitive settings are using defaults. +import logging as _logging + +_startup_logger = _logging.getLogger("config") + +if settings.l402_hmac_secret == "timmy-hmac-secret": + _startup_logger.warning( + "SEC: L402_HMAC_SECRET is using the default value — " + "set a unique secret in .env before deploying to production." + ) +if settings.l402_macaroon_secret == "timmy-macaroon-secret": + _startup_logger.warning( + "SEC: L402_MACAROON_SECRET is using the default value — " + "set a unique secret in .env before deploying to production." + ) diff --git a/src/lightning/factory.py b/src/lightning/factory.py index 44b262d..f0e2eb7 100644 --- a/src/lightning/factory.py +++ b/src/lightning/factory.py @@ -12,6 +12,7 @@ import logging import os from typing import Optional +from config import settings from lightning.base import LightningBackend logger = logging.getLogger(__name__) @@ -68,7 +69,7 @@ def get_backend(name: Optional[str] = None) -> LightningBackend: """ _register_backends() - backend_name = (name or os.environ.get("LIGHTNING_BACKEND", "mock")).lower() + backend_name = (name or settings.lightning_backend).lower() if backend_name not in _BACKENDS: available = ", ".join(_BACKENDS.keys()) @@ -100,8 +101,8 @@ def get_backend_info() -> dict: Returns: Dict with backend info for health/status endpoints """ - backend_name = os.environ.get("LIGHTNING_BACKEND", "mock") - + backend_name = settings.lightning_backend + return { "configured_backend": backend_name, "available_backends": list_backends(), diff --git a/src/lightning/mock_backend.py b/src/lightning/mock_backend.py index e75a0d3..9849151 100644 --- a/src/lightning/mock_backend.py +++ b/src/lightning/mock_backend.py @@ -12,20 +12,13 @@ import secrets import time from typing import Optional +from config import settings from lightning.base import Invoice, LightningBackend, LightningError logger = logging.getLogger(__name__) -# Secret for HMAC-based invoice verification (mock mode) -_HMAC_SECRET_DEFAULT = "timmy-sovereign-sats" -_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT) -_HMAC_SECRET = _HMAC_SECRET_RAW.encode() - -if _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT: - logger.warning( - "SEC: L402_HMAC_SECRET is using the default value — set a unique " - "secret in .env before deploying to production." - ) +# Read secret from centralised config (validated at startup in config.py) +_HMAC_SECRET = settings.l402_hmac_secret.encode() class MockBackend(LightningBackend): diff --git a/src/timmy_serve/l402_proxy.py b/src/timmy_serve/l402_proxy.py index 461aa51..3b06c91 100644 --- a/src/timmy_serve/l402_proxy.py +++ b/src/timmy_serve/l402_proxy.py @@ -13,29 +13,20 @@ import base64 import hashlib import hmac import logging -import os import time from dataclasses import dataclass from typing import Optional +from config import settings from timmy_serve.payment_handler import payment_handler logger = logging.getLogger(__name__) -_MACAROON_SECRET_DEFAULT = "timmy-macaroon-secret" -_MACAROON_SECRET_RAW = os.environ.get("L402_MACAROON_SECRET", _MACAROON_SECRET_DEFAULT) -_MACAROON_SECRET = _MACAROON_SECRET_RAW.encode() - -_HMAC_SECRET_DEFAULT = "timmy-hmac-secret" -_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT) +# Read secrets from centralised config (validated at startup in config.py) +_MACAROON_SECRET = settings.l402_macaroon_secret.encode() +_HMAC_SECRET_RAW = settings.l402_hmac_secret _HMAC_SECRET = _HMAC_SECRET_RAW.encode() -if _MACAROON_SECRET_RAW == _MACAROON_SECRET_DEFAULT or _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT: - logger.warning( - "SEC: L402 secrets are using default values — set L402_MACAROON_SECRET " - "and L402_HMAC_SECRET in .env before deploying to production." - ) - @dataclass class Macaroon: