Skip to content

Commit b330daa

Browse files
feat: add bootstrap API key creation on startup
When WMG_BOOTSTRAP_API_KEY env var is set, the server creates an admin/runs:write API key on first startup if no keys exist yet. This enables production write access without shell access. Added to render.yaml with generateValue for secure auto-generation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c437e1 commit b330daa

File tree

4 files changed

+32
-2
lines changed

4 files changed

+32
-2
lines changed

render.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ services:
2626
value: "false"
2727
- key: WMG_UPLOAD_TOKEN
2828
generateValue: true
29+
- key: WMG_BOOTSTRAP_API_KEY
30+
generateValue: true

server/worldmodel_server/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(self) -> None:
6464
self.seed_demo_data = _as_bool(os.getenv("WMG_SEED_DEMO_DATA"), False)
6565
self.log_json = _as_bool(os.getenv("WMG_LOG_JSON"), True)
6666
self.log_level = os.getenv("WMG_LOG_LEVEL", "INFO").upper()
67+
self.bootstrap_api_key = os.getenv("WMG_BOOTSTRAP_API_KEY", "")
6768

6869
@property
6970
def is_production(self) -> bool:

server/worldmodel_server/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from worldmodel_server.rate_limit import rate_limiter
2020
from worldmodel_server.request_logging import configure_logging, log_request_event
2121
from worldmodel_server.schemas import LeaderboardRow, RunCreate, RunResponse
22-
from worldmodel_server.seed import seed_demo_runs
22+
from worldmodel_server.seed import bootstrap_api_key, seed_demo_runs
2323
from worldmodel_server.storage import (
2424
artifact_key,
2525
ensure_storage_dirs,
@@ -53,6 +53,9 @@ async def lifespan(_app: FastAPI):
5353
print("[lifespan] Migrations complete", flush=True)
5454
ensure_storage_dirs()
5555
print("[lifespan] Storage dirs ensured", flush=True)
56+
with SessionLocal() as session:
57+
if bootstrap_api_key(session):
58+
print("[lifespan] Bootstrap API key created", flush=True)
5659
if settings.seed_demo_data:
5760
print("[lifespan] Seeding demo data...", flush=True)
5861
with SessionLocal() as session:

server/worldmodel_server/seed.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sqlalchemy import select
77
from sqlalchemy.orm import Session
88

9-
from worldmodel_server.models import RunEntry
9+
from worldmodel_server.models import ApiKey, RunEntry
1010
from worldmodel_server.storage import save_run_artifact, storage_status
1111

1212
DEMO_RUNS = [
@@ -126,3 +126,27 @@ def seed_demo_runs(session: Session, *, force: bool = False) -> int:
126126

127127
session.commit()
128128
return created_count
129+
130+
131+
def bootstrap_api_key(session: Session) -> bool:
132+
"""Create a production API key from WMG_BOOTSTRAP_API_KEY if no keys exist yet.
133+
134+
Returns True if a new key was created, False otherwise.
135+
"""
136+
from worldmodel_server.auth import create_api_key as _create_api_key
137+
from worldmodel_server.config import settings
138+
139+
if not settings.bootstrap_api_key:
140+
return False
141+
142+
existing = session.scalars(select(ApiKey.id).limit(1)).first()
143+
if existing is not None:
144+
return False
145+
146+
_create_api_key(
147+
session,
148+
name="prod-writer",
149+
scopes=["admin", "runs:write"],
150+
raw_key=settings.bootstrap_api_key,
151+
)
152+
return True

0 commit comments

Comments
 (0)