| |
| """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" |
|
|
| |
| |
| |
| |
| REQUIRED_KEYS: list[str] = [ |
| |
| "SUPABASE_JWT_SECRET", |
| "SUPABASE_SERVICE_ROLE_KEY", |
| "NEXT_PUBLIC_SUPABASE_URL", |
| |
| "HF_TOKEN", |
| |
| "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", |
| |
| "IBM_QUANTUM_API_KEY", |
| |
| |
| |
| |
| |
| "CRON_SECRET", |
| "BEE_VERCEL_URL", |
| |
| |
| |
| "SENTRY_DSN_BACKEND", |
| ] |
|
|
| |
| |
| 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): |
| |
| 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()) |
|
|