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
This commit is contained in:
Claude
2026-02-25 18:19:22 +00:00
parent c0ca166d43
commit 2e7f3d1b29
7 changed files with 70 additions and 35 deletions

3
.gitignore vendored
View File

@@ -21,6 +21,9 @@ env/
# SQLite memory — never commit agent memory # SQLite memory — never commit agent memory
*.db *.db
# Runtime PID files
.watchdog.pid
# Chat platform state files (contain bot tokens) # Chat platform state files (contain bot tokens)
telegram_state.json telegram_state.json
discord_state.json discord_state.json

View File

@@ -27,14 +27,14 @@ install-bigbrain: $(VENV)/bin/activate
install-creative: $(VENV)/bin/activate install-creative: $(VENV)/bin/activate
$(PIP) install --quiet -e ".[dev,creative]" $(PIP) install --quiet -e ".[dev,creative]"
@if [ "$$(uname -s)" = "Darwin" ]; then \ @if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname -s)" = "Darwin" ]; then \
echo ""; \ echo " Apple Silicon detected — installing PyTorch with Metal (MPS) support..."; \
echo " Note: PyTorch on macOS uses CPU by default."; \ $(PIP) install --quiet --pre torch torchvision torchaudio \
echo " For Metal (GPU) acceleration, install the nightly build:"; \ --index-url https://download.pytorch.org/whl/nightly/cpu; \
echo " pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu"; \ echo "✓ Creative extras installed with Metal GPU acceleration"; \
echo ""; \ else \
echo "✓ Creative extras installed (diffusers, torch, ace-step)"; \
fi fi
@echo "✓ Creative extras installed (diffusers, torch, ace-step)"
$(VENV)/bin/activate: $(VENV)/bin/activate:
python3 -m venv $(VENV) python3 -m venv $(VENV)

View File

@@ -60,15 +60,37 @@ python -m pytest "$REPO_DIR/tests/" -q --tb=short
echo "==> All tests passed." echo "==> All tests passed."
# ── 4. Self-TDD watchdog (background) ──────────────────────────────────────── # ── 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..." echo "==> Starting self-TDD watchdog (60s interval) in background..."
self-tdd watch --interval 60 & self-tdd watch --interval 60 &
WATCHDOG_PID=$! 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" 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 ───────────────────────────────────────────────────────────── # ── 5. Dashboard ─────────────────────────────────────────────────────────────
echo "" echo ""
echo "==> Starting Timmy Time dashboard at http://localhost:8000" 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 "" echo ""
uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000

View File

@@ -59,6 +59,14 @@ class Settings(BaseSettings):
video_transition_duration: float = 1.0 video_transition_duration: float = 1.0
default_video_codec: str = "libx264" 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( model_config = SettingsConfigDict(
env_file=".env", env_file=".env",
env_file_encoding="utf-8", env_file_encoding="utf-8",
@@ -67,3 +75,20 @@ class Settings(BaseSettings):
settings = Settings() 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."
)

View File

@@ -12,6 +12,7 @@ import logging
import os import os
from typing import Optional from typing import Optional
from config import settings
from lightning.base import LightningBackend from lightning.base import LightningBackend
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ def get_backend(name: Optional[str] = None) -> LightningBackend:
""" """
_register_backends() _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: if backend_name not in _BACKENDS:
available = ", ".join(_BACKENDS.keys()) available = ", ".join(_BACKENDS.keys())
@@ -100,8 +101,8 @@ def get_backend_info() -> dict:
Returns: Returns:
Dict with backend info for health/status endpoints Dict with backend info for health/status endpoints
""" """
backend_name = os.environ.get("LIGHTNING_BACKEND", "mock") backend_name = settings.lightning_backend
return { return {
"configured_backend": backend_name, "configured_backend": backend_name,
"available_backends": list_backends(), "available_backends": list_backends(),

View File

@@ -12,20 +12,13 @@ import secrets
import time import time
from typing import Optional from typing import Optional
from config import settings
from lightning.base import Invoice, LightningBackend, LightningError from lightning.base import Invoice, LightningBackend, LightningError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Secret for HMAC-based invoice verification (mock mode) # Read secret from centralised config (validated at startup in config.py)
_HMAC_SECRET_DEFAULT = "timmy-sovereign-sats" _HMAC_SECRET = settings.l402_hmac_secret.encode()
_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."
)
class MockBackend(LightningBackend): class MockBackend(LightningBackend):

View File

@@ -13,29 +13,20 @@ import base64
import hashlib import hashlib
import hmac import hmac
import logging import logging
import os
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from config import settings
from timmy_serve.payment_handler import payment_handler from timmy_serve.payment_handler import payment_handler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_MACAROON_SECRET_DEFAULT = "timmy-macaroon-secret" # Read secrets from centralised config (validated at startup in config.py)
_MACAROON_SECRET_RAW = os.environ.get("L402_MACAROON_SECRET", _MACAROON_SECRET_DEFAULT) _MACAROON_SECRET = settings.l402_macaroon_secret.encode()
_MACAROON_SECRET = _MACAROON_SECRET_RAW.encode() _HMAC_SECRET_RAW = settings.l402_hmac_secret
_HMAC_SECRET_DEFAULT = "timmy-hmac-secret"
_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT)
_HMAC_SECRET = _HMAC_SECRET_RAW.encode() _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 @dataclass
class Macaroon: class Macaroon: