#!/usr/bin/env python3 """Praxis v0.4 — Operator bootstrap CLI (TASK-05-01, D-052). Creates the initial operator from env-provided credentials. Idempotent (ON CONFLICT DO NOTHING). The --update flag forces a rehash + update. Env: PRAXIS_BOOTSTRAP_OPERATOR_USER — operator username (required) PRAXIS_BOOTSTRAP_OPERATOR_PASS — operator password (required) PRAXIS_PG_DSN — Postgres DSN (required) Exit: 0 on success (created or already-exists), 1 on missing env / DB error. Retries on connection failure (3 attempts, 5s backoff — R-BOOT-01). Run: PRAXIS_BOOTSTRAP_OPERATOR_USER=admin PRAXIS_BOOTSTRAP_OPERATOR_PASS=... \ PRAXIS_PG_DSN=postgresql://praxis:...@postgres:5432/praxis \ python3 scripts/create-operator.py """ from __future__ import annotations import argparse import asyncio import os import sys from argon2 import PasswordHasher _ph = PasswordHasher() _RETRY_ATTEMPTS = 3 _RETRY_BACKOFF_S = 5.0 async def create_operator(update: bool = False) -> int: user = os.environ.get("PRAXIS_BOOTSTRAP_OPERATOR_USER", "").strip() pw = os.environ.get("PRAXIS_BOOTSTRAP_OPERATOR_PASS", "") dsn = os.environ.get("PRAXIS_PG_DSN", "").strip() if not user or not pw: print( "create-operator: ERROR — PRAXIS_BOOTSTRAP_OPERATOR_USER and " "PRAXIS_BOOTSTRAP_OPERATOR_PASS must be set (R-BOOT-02).", file=sys.stderr, ) return 1 if not dsn: print( "create-operator: ERROR — PRAXIS_PG_DSN must be set.", file=sys.stderr, ) return 1 import asyncpg from db.pg_migrate import apply_pg_migrations from db.pg_store import PgStore last_exc: Exception | None = None for attempt in range(1, _RETRY_ATTEMPTS + 1): try: pool = await asyncpg.create_pool( dsn=dsn, min_size=1, max_size=3, command_timeout=10 ) try: await apply_pg_migrations(pool) store = PgStore(pool) pw_hash = _ph.hash(pw) display = user oid = await store.insert_operator( user, pw_hash, display, on_conflict_update=update ) if update: print(f"create-operator: updated operator {user!r} (id={oid})") elif oid is not None: print(f"create-operator: created operator {user!r} (id={oid})") else: print(f"create-operator: operator {user!r} already exists (no change)") return 0 finally: await pool.close() except (asyncpg.PostgresConnectionError, ConnectionError, OSError) as exc: last_exc = exc if attempt < _RETRY_ATTEMPTS: print( f"create-operator: connection attempt {attempt} failed " f"({exc}); retrying in {_RETRY_BACKOFF_S}s (R-BOOT-01)...", file=sys.stderr, ) await asyncio.sleep(_RETRY_BACKOFF_S) continue print(f"create-operator: ERROR — could not connect after {_RETRY_ATTEMPTS} " f"attempts: {last_exc}", file=sys.stderr) return 1 def main() -> int: parser = argparse.ArgumentParser(description="Create the initial Praxis operator.") parser.add_argument( "--update", action="store_true", help="Force rehash + update if the operator already exists.", ) args = parser.parse_args() return asyncio.run(create_operator(update=args.update)) if __name__ == "__main__": raise SystemExit(main())