#!/usr/bin/env python3 """scripts/setup_modal_secrets.py — push Bee's prod env into a Modal Secret. Reads `.env` (gitignored) and creates / replaces the `bee-prod` Modal secret with the keys bee/server.py + bee/auth.py + bee/teacher_providers.py need at boot. Idempotent: re-run after rotating any key. Re-running with `--force` overwrites the existing Modal secret in place. Usage: python3 scripts/setup_modal_secrets.py python3 scripts/setup_modal_secrets.py --force python3 scripts/setup_modal_secrets.py --dry-run Implementation: shells out to `modal secret create` because the Python SDK's secret-create surface requires escaping shenanigans for multiline values. The CLI handles it cleanly. """ from __future__ import annotations import argparse import os import shlex import subprocess import sys from pathlib import Path try: from dotenv import dotenv_values except ImportError: print("ERROR: python-dotenv not installed; pip install python-dotenv", file=sys.stderr) sys.exit(1) REPO_ROOT = Path(__file__).resolve().parent.parent ENV_FILE = REPO_ROOT / ".env" # The exhaustive list of env vars Bee's runtime reads. Keep this in sync # with bee/server.py (server runtime), bee/auth.py (JWT verification), # bee/teacher_providers.py (adaptive-router teacher chain), and any new # providers wired in later. Missing keys = 401s in prod. REQUIRED_KEYS: list[str] = [ # ── Auth ─────────────────────────────────────────────────────────── "SUPABASE_JWT_SECRET", "SUPABASE_SERVICE_ROLE_KEY", "NEXT_PUBLIC_SUPABASE_URL", # ── Hugging Face (model + adapter pulls + dataset writes) ───────── "HF_TOKEN", # ── Teacher chain for the adaptive router escalation path ───────── "BEE_TEACHER_PROVIDER", "BEE_DEEPSEEK_API_KEY", "BEE_TEACHER_API_KEY", "BEE_OPENAI_API_KEY", "BEE_GOOGLE_API_KEY", "BEE_OLLAMA_API_KEY", "BEE_OPENROUTER_API_KEY", "BEE_MISTRAL_API_KEY", # ── Quantum (only matters when BEE_IGNITE=1; harmless when off) ─── "IBM_QUANTUM_API_KEY", # ── Research-queue capture (Modal worker → Vercel /api/research/capture) # CRON_SECRET is the shared bearer the endpoint accepts; BEE_VERCEL_URL # overrides NEXT_PUBLIC_SITE_URL (which is localhost in dev) so the # Modal container POSTs to the production Vercel host. Missing keys # → captures silently no-op (best-effort). "CRON_SECRET", "BEE_VERCEL_URL", # ── Sentry (bee-backend project) — error tracking for Modal serverless. # Free-tier discipline: errors only, no traces. bee/server.py reads # SENTRY_DSN_BACKEND at boot; missing key → Sentry init no-ops cleanly. "SENTRY_DSN_BACKEND", ] # Optional keys — pushed if present, ignored if missing. Mostly future- # proofing for paths bee/server.py reads but not all deploys need. OPTIONAL_KEYS: list[str] = [ "BEE_API_KEYS", "BEE_BASE_MODEL", "BEE_MODEL_PATH", "BEE_DEEPSEEK_MODEL", "BEE_OPENAI_MODEL", "BEE_GOOGLE_MODEL", "BEE_ANTHROPIC_MODEL", "BEE_OLLAMA_MODEL", "BEE_MISTRAL_MODEL", "BEE_OPENROUTER_MODEL", ] def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( "--force", action="store_true", help="overwrite the existing bee-prod secret (default fails if it exists)", ) parser.add_argument( "--dry-run", action="store_true", help="print which keys would be pushed and exit", ) args = parser.parse_args() if not ENV_FILE.exists(): print(f"ERROR: .env not found at {ENV_FILE}", file=sys.stderr) return 1 env = dotenv_values(ENV_FILE) missing = [k for k in REQUIRED_KEYS if not (env.get(k) or "").strip()] if missing: print(f"ERROR: required keys missing from .env: {missing}", file=sys.stderr) return 1 push: dict[str, str] = {} for k in REQUIRED_KEYS: v = (env.get(k) or "").strip() if v: push[k] = v for k in OPTIONAL_KEYS: v = (env.get(k) or "").strip() if v: push[k] = v print(f"Will push {len(push)} keys to Modal secret `bee-prod`:") for k in sorted(push): # Redact value display v = push[k] masked = v[:6] + "***" if len(v) > 8 else "***" print(f" {k:<32} = {masked}") if args.dry_run: print("dry-run; not pushing") return 0 cmd = ["modal", "secret", "create"] if args.force: cmd.append("--force") cmd.append("bee-prod") for k, v in push.items(): cmd.append(f"{k}={v}") print() print(f"Running: modal secret create {'--force ' if args.force else ''}bee-prod ...") result = subprocess.run(cmd, capture_output=True, text=True) if result.stdout: print(result.stdout.strip()) if result.returncode != 0: print(result.stderr.strip(), file=sys.stderr) return result.returncode print("Modal secret `bee-prod` created.") print(f"Verify with: modal secret list | grep bee-prod") return 0 if __name__ == "__main__": sys.exit(main())