bee / scripts /setup_modal_secrets.py
Bee Deploy
HF Space backend deploy [de0cba5]
5e21013
#!/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())