Files
praxis/scripts/create-operator.py
T
Praxis CI f2a12f9fed docs(milestone): complete v0.4-operator-tier — v0.1.9 tagged, milestone release, merged to main
v0.4 (Operator Tier — Cohort Dashboard + Auth + Postgres) milestone complete.

Phases:
  ✓ P0  pre-execution (planning)        → v0.1.6
  ✓ P1  operator foundation (Postgres+auth+VC migration) → v0.1.7
  ✓ P2  cohort dashboard + aggregation   → v0.1.8
  ✓ P3  final review + ship              → v0.1.9 (= v0.4 milestone release)

Requirements covered (8/8):
  REQ-MT-01 (Postgres store), REQ-MT-02 (aggregation pipeline),
  REQ-AUTH-01 (operator auth), REQ-DASH-01 (cohort dashboard),
  REQ-NFR-AUTH-01 (auth NFRs), REQ-NFR-MT-01 (Postgres-in-LXC),
  REQ-NFR-DASH-01 (k-anonymity ≥10), REQ-NFR-DASH-02 (freshness ≤24h)

Grill MUSTs honored (6/6): G-008, G-011, G-027, G-031, G-038, G-041

Tests: 317 pytest pass, 36 skip (Postgres-requiring), 0 fail; 17/17 vitest pass
Review: APPROVE_WITH_NOTES (6/6 personas, 0 P0, 8 P1+ carry-forward)
Audit: HEALTHY (reconstruction PASS, 8/8 REQ, 6/6 grill)

---ci---
project: praxis
phase: 3
milestone: v0.4
status: complete
phase_role: final
milestone_complete: true
milestone_merged_to_main: true
tag: v0.1.9
requirements:
  covered: [REQ-MT-01, REQ-MT-02, REQ-AUTH-01, REQ-DASH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01, REQ-NFR-DASH-01, REQ-NFR-DASH-02]
  partial: []
---/ci---
2026-08-04 11:58:44 +00:00

106 lines
3.6 KiB
Python
Executable File

#!/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())