"""Operator API for the bounded Enrollment & Provisioning Plane v1.""" from __future__ import annotations from typing import Annotated, Literal from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request from pydantic import BaseModel, ConfigDict from sqlmodel import Session from ..core.config import settings from ..core.security import require_operator_request from ..db.session import get_session from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1 from ..services.enrollment_plane import EnrollmentError, EnrollmentPlane from ..services.repopulse_adoption_journal import ( RepoPulseAdoptionJournalError, get_repopulse_adoption_status, ) def _require_operator(request: Request) -> str: if not settings.operator_auth_required: raise HTTPException(status_code=503, detail="Enrollment requires the configured operator authentication boundary.") return require_operator_request(request) router = APIRouter( prefix="/api/enrollments", tags=["enrollments"], dependencies=[Depends(_require_operator)], ) class EnrollmentRequestV1(BaseModel): model_config = ConfigDict(extra="forbid") plan: EnrollmentPlanV1 operation: Literal["preflight", "enroll"] = "preflight" EnrollmentSession = Annotated[Session, Depends(get_session)] EnrollmentIdempotencyKey = Annotated[str | None, Header(alias="Idempotency-Key")] EnrollmentLimit = Annotated[int, Query(ge=1, le=250)] def _idempotency(value: str | None) -> str: if not value: raise HTTPException(status_code=400, detail="Idempotency-Key is required for enrollment mutations.") return value.strip() @router.get("/capabilities") def enrollment_capabilities(session: EnrollmentSession): return EnrollmentPlane().capabilities(session) @router.get("/trust") def enrollment_trust_status(session: EnrollmentSession): return EnrollmentPlane.trust_status(session) @router.put("/trust") def update_enrollment_trust(payload: EnrollmentTrustConfigV1, session: EnrollmentSession): return EnrollmentPlane.set_trust_config(session, payload) @router.post("") def request_enrollment( payload: EnrollmentRequestV1, session: EnrollmentSession, idempotency_key: EnrollmentIdempotencyKey = None, ): plane = EnrollmentPlane() try: if payload.operation == "preflight": return plane.preflight(session, payload.plan) return plane.enroll(session, payload.plan, idempotency_key=_idempotency(idempotency_key)) except EnrollmentError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.get("") def list_enrollments(session: EnrollmentSession, limit: EnrollmentLimit = 100): return {"items": EnrollmentPlane().list_enrollments(session, limit=limit)} @router.get("/repopulse/adoption") def repopulse_adoption_status(session: EnrollmentSession): """Read the sole fixed migration journal through the operator boundary.""" try: return get_repopulse_adoption_status(session) except RepoPulseAdoptionJournalError as exc: raise HTTPException(status_code=503, detail=exc.code) from None @router.get("/{enrollment_id}") def get_enrollment(enrollment_id: str, session: EnrollmentSession): try: return EnrollmentPlane().get_enrollment(session, enrollment_id) except EnrollmentError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc