from __future__ import annotations import argparse import asyncio import json import os import uuid from datetime import UTC, datetime, timedelta from pathlib import Path import yaml from sqlalchemy import select, text, update from app.config import validate_config from app.db import ConfigVersion, SafetyAudit, engine_and_sessions from app.settings import _secret async def execute(args: argparse.Namespace) -> None: url = _secret("MESSAGE_SAFETY_CONFIG_ADMIN_DATABASE_URL") assert url artifacts = Path(os.getenv("MESSAGE_SAFETY_ARTIFACTS_DIR", "/app/app/artifacts")) document = ( yaml.safe_load(await asyncio.to_thread(Path(args.file).read_text, encoding="utf-8")) if args.file else None ) if document: _, _, digest = validate_config(document, artifacts) engine, sessions = engine_and_sessions(url) try: if args.command == "validate": print(json.dumps({"valid": True, "config_sha256": digest.hex()})) return async with sessions.begin() as session: await session.execute( text("SELECT pg_advisory_xact_lock(hashtext('message_safety.config_activation'))") ) if args.command == "create": exists = await session.scalar( select(ConfigVersion.id).where(ConfigVersion.version == args.version) ) if exists: raise ValueError("config version already exists") session.add( ConfigVersion( version=args.version, schema_version=document["schema_version"], state="draft", config=document, config_sha256=digest, created_by=args.actor, created_at=datetime.now(UTC), ) ) elif args.command == "activate": row = await session.scalar( select(ConfigVersion) .where(ConfigVersion.version == args.version) .with_for_update() ) if not row or row.state != "draft": raise ValueError("only a draft config can be activated") validate_config(row.config, artifacts) now = datetime.now(UTC) await session.execute( update(ConfigVersion) .where(ConfigVersion.state == "active") .values(state="retired", retired_at=now) ) row.state = "active" row.approved_by = args.approved_by row.approved_at = now row.activated_at = now session.add( SafetyAudit( id=uuid.uuid4(), event="config_activated", processing_mode="standard", config_version=row.version, created_at=now, purge_after=now + timedelta(days=180), ) ) print(json.dumps({"ok": True, "version": args.version})) finally: await engine.dispose() def parser() -> argparse.ArgumentParser: result = argparse.ArgumentParser() commands = result.add_subparsers(dest="command", required=True) validate = commands.add_parser("validate") validate.add_argument("file") create = commands.add_parser("create") create.add_argument("file") create.add_argument("--version", type=int, required=True) create.add_argument("--actor", required=True) activate = commands.add_parser("activate") activate.add_argument("--version", type=int, required=True) activate.add_argument("--approved-by", required=True) activate.set_defaults(file=None) return result def main() -> None: asyncio.run(execute(parser().parse_args())) if __name__ == "__main__": main()