"""Standalone secure HTTP boundary for G8 Continuous Improvement OS.""" from __future__ import annotations import secrets from collections.abc import AsyncIterator from datetime import datetime from typing import Annotated, Literal from uuid import UUID import asyncpg from fastapi import APIRouter, Depends, Header, HTTPException, status from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from .. import db from ..config import Settings, get_settings from ..contracts.continuous_improvement import ( AgenticReleaseManifest, ContentBenchmarkQualification, ContentSourceArtifact, GeneratedContentDraft, IndependentRedTeamReview, ModelCalibrationSnapshot, OperationalIncident, ) from ..deps import Principal, Role, require_role from ..engine_client import engine_client from ..services import continuous_improvement_store from ..services import continuous_improvement_agentic router = APIRouter(tags=["continuous-improvement"]) INTERNAL_TOKEN_HEADER = "X-Vignette-Continuous-Improvement-Token" MIN_INTERNAL_TOKEN_LENGTH = 32 SyntheticDataClassification = Literal["synthetic_replay_red_team_coverage_drift"] async def _research_db_provider() -> AsyncIterator[asyncpg.Connection]: async with db.acquire(ai_view="research", ai_context=True) as conn: yield conn def _authenticate_internal(settings: Settings, presented_token: str | None) -> None: configured_token = settings.continuous_improvement_internal_token.get_secret_value() if len(configured_token) < MIN_INTERNAL_TOKEN_LENGTH: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="continuous improvement automation is unavailable", ) if presented_token is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="internal authentication required", ) if not secrets.compare_digest(presented_token, configured_token): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="internal authentication failed", ) async def continuous_improvement_internal_db( settings: Annotated[Settings, Depends(get_settings)], presented_token: Annotated[str | None, Header(alias=INTERNAL_TOKEN_HEADER)] = None, ) -> AsyncIterator[asyncpg.Connection]: """Fail closed before acquiring the research-view database connection.""" _authenticate_internal(settings, presented_token) async for conn in _research_db_provider(): yield conn ResearchDB = Annotated[asyncpg.Connection, Depends(continuous_improvement_internal_db)] AdminPrincipal = Annotated[Principal, Depends(require_role(Role.ADMIN))] async def continuous_improvement_admin_db( principal: AdminPrincipal, ) -> AsyncIterator[asyncpg.Connection]: """Acquire DB state with the effective admin role selected by the role gate.""" async with db.acquire( role=Role.ADMIN.value, user_id=principal.user_id, cohort_ids=principal.cohort_ids, ) as conn: yield conn AdminDB = Annotated[asyncpg.Connection, Depends(continuous_improvement_admin_db)] class ContentPipelineRequest(BaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) submission_id: UUID pipeline_id: UUID benchmark_record_id: UUID qualification_id: UUID data_classification: SyntheticDataClassification draft: GeneratedContentDraft sources: list[ContentSourceArtifact] = Field(min_length=1, max_length=100) reviews: list[IndependentRedTeamReview] = Field(min_length=2, max_length=20) benchmark: ContentBenchmarkQualification class ContentPipelineResponse(BaseModel): submission_id: UUID pipeline_id: UUID qualification_id: UUID candidate_catalog_entry_id: str state: Literal["pending_human_approval"] human_approval_required: Literal[True] catalog_promoted: Literal[False] idempotent_replay: bool clinical_claim_allowed: Literal[False] = False class AgenticContentPipelineRequest(BaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) submission_id: UUID pipeline_id: UUID benchmark_record_id: UUID qualification_id: UUID data_classification: SyntheticDataClassification source_packs: list[continuous_improvement_agentic.AgenticSourcePack] = Field( min_length=1, max_length=20 ) content_kind: Literal["case", "rupture", "practice", "benchmark"] difficulty_level: int = Field(ge=1, le=5) variant_count: int = Field(default=3, ge=3, le=12) prompt_version: str = Field(default="1.0.0", min_length=1, max_length=80) @model_validator(mode="after") def unique_source_packs(self) -> "AgenticContentPipelineRequest": source_ids = [item.artifact.source_id for item in self.source_packs] if len(source_ids) != len(set(source_ids)): raise ValueError("agentic source pack ids must be unique") return self class AgenticContentPipelineResponse(ContentPipelineResponse): draft_id: str benchmark_id: str red_team_review_count: int = Field(ge=2) benchmark_variant_count: int = Field(ge=3) agent_calls_executed: int = Field(ge=0) trigger_kind: Literal["source_pack", "operational_incident"] class IncidentAdversarialPipelineRequest(BaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) submission_id: UUID pipeline_id: UUID benchmark_record_id: UUID qualification_id: UUID data_classification: SyntheticDataClassification difficulty_level: int = Field(default=5, ge=1, le=5) variant_count: int = Field(default=3, ge=3, le=12) prompt_version: str = Field(default="1.0.0", min_length=1, max_length=80) class GateArtifact(BaseModel): model_config = ConfigDict(extra="forbid") artifact_record_id: UUID artifact_id: str = Field(min_length=1, max_length=180) content_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") provenance_uri: str = Field(pattern=r"^(repo|db|audit)://[a-zA-Z0-9_./:-]+$") class CompleteGateArtifacts(BaseModel): model_config = ConfigDict(extra="forbid") baseline: GateArtifact threshold: GateArtifact provenance: list[GateArtifact] = Field(min_length=1, max_length=100) rollback: GateArtifact @model_validator(mode="after") def artifact_ids_are_unique(self) -> "CompleteGateArtifacts": values = [ self.baseline.artifact_record_id, self.threshold.artifact_record_id, *(item.artifact_record_id for item in self.provenance), self.rollback.artifact_record_id, ] if len(values) != len(set(values)): raise ValueError("gate artifact UUIDs must be unique") return self class ModelChangeGateRequest(BaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) submission_id: UUID gate_id: UUID baseline_snapshot_record_id: UUID candidate_snapshot_record_id: UUID data_classification: SyntheticDataClassification baseline: ModelCalibrationSnapshot candidate: ModelCalibrationSnapshot artifacts: CompleteGateArtifacts @model_validator(mode="after") def baseline_and_candidate_are_distinct(self) -> "ModelChangeGateRequest": if self.baseline.snapshot_id == self.candidate.snapshot_id: raise ValueError("baseline and candidate snapshots must be distinct") if self.baseline_snapshot_record_id == self.candidate_snapshot_record_id: raise ValueError("baseline and candidate record UUIDs must be distinct") return self class ModelChangeGateResponse(BaseModel): submission_id: UUID gate_id: UUID gate_decision: Literal["promote", "rollback", "quarantine"] state: Literal["pending_human_approval"] human_approval_required: Literal[True] promotion_executed: Literal[False] idempotent_replay: bool class ReleaseGateRequest(BaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) submission_id: UUID gate_id: UUID data_classification: SyntheticDataClassification manifest: AgenticReleaseManifest artifacts: CompleteGateArtifacts class ReleaseGateResponse(BaseModel): submission_id: UUID gate_id: UUID qualified: bool state: Literal["pending_human_approval"] human_approval_required: Literal[True] promotion_executed: Literal[False] idempotent_replay: bool class IncidentDagRequest(BaseModel): model_config = ConfigDict(extra="forbid") submission_id: UUID incident_record_id: UUID data_classification: SyntheticDataClassification incident: OperationalIncident class IncidentDagResponse(BaseModel): submission_id: UUID incident_record_id: UUID node_count: Literal[4] idempotent_replay: bool pii_included: Literal[False] = False class HumanApprovalRequest(BaseModel): model_config = ConfigDict(extra="forbid") submission_id: UUID approval_event_id: UUID effect_record_id: UUID target_kind: Literal["content_qualification", "model_change_gate", "release_gate"] target_id: UUID decision: Literal[ "approve_content", "approve_promotion", "authorize_rollback", "reject", "keep_quarantine", ] reason_code: str = Field(min_length=1, max_length=180) evidence_refs: list[str] = Field(min_length=1, max_length=100) @field_validator("evidence_refs") @classmethod def evidence_refs_are_unique(cls, value: list[str]) -> list[str]: if len(value) != len(set(value)): raise ValueError("approval evidence refs must be unique") return value class HumanApprovalResponse(BaseModel): submission_id: UUID approval_event_id: UUID target_kind: str target_id: UUID decision: str effect_record_id: UUID idempotent_replay: bool class MonitorEventRequest(BaseModel): model_config = ConfigDict(extra="forbid") submission_id: UUID lifecycle_event_id: UUID data_classification: SyntheticDataClassification target_kind: Literal["model_change_gate", "release_gate"] target_id: UUID event_status: Literal[ "healthy", "drift_detected", "rollback_recommended", "rollback_verified" ] evidence_refs: list[str] = Field(min_length=1, max_length=100) class MonitorEventResponse(BaseModel): submission_id: UUID lifecycle_event_id: UUID event_status: str idempotent_replay: bool class CatalogGroundedClaim(BaseModel): model_config = ConfigDict(extra="forbid") claim: str = Field(min_length=1, max_length=600) source_ref: str = Field(min_length=1, max_length=180) class CatalogVisiblePayload(BaseModel): """Explicit allowlist for content that may cross the approved catalog boundary.""" model_config = ConfigDict(extra="forbid") title: str = Field(min_length=1, max_length=180) synthetic_profile: str = Field(min_length=1, max_length=1200) scenario: str = Field(min_length=1, max_length=6000) rupture_or_challenge: str = Field(min_length=1, max_length=2400) learner_task: str = Field(min_length=1, max_length=2000) success_criteria: list[str] = Field(min_length=1, max_length=10) source_refs: list[str] = Field(min_length=1, max_length=100) grounded_claims: list[CatalogGroundedClaim] = Field(min_length=1, max_length=20) class ContentQualificationView(BaseModel): qualification_id: UUID pipeline_id: UUID catalog_entry_id: str payload_sha256: str content_kind: Literal["case", "rupture", "practice", "benchmark"] difficulty_level: int = Field(ge=1, le=5) synthetic_identity_id: str source_count: int = Field(ge=1) red_team_review_count: int = Field(ge=2) benchmark_variant_count: int = Field(ge=3) benchmark_pass_rate: float = Field(ge=0.85, le=1.0) source_provenance_uris: list[str] = Field(min_length=1) draft_payload: CatalogVisiblePayload | None = None gate_state: Literal["pending_human_approval"] created_at: datetime class ModelChangeGateView(BaseModel): gate_id: UUID gate_decision: Literal["promote", "rollback", "quarantine"] reasons: list[str] state: Literal["pending_human_approval"] created_at: datetime class ReleaseGateView(BaseModel): gate_id: UUID release_id: str qualified: bool state: Literal["pending_human_approval"] created_at: datetime class GateArtifactView(BaseModel): artifact_record_id: UUID owner_kind: Literal["model_change_gate", "release_gate"] owner_id: UUID artifact_kind: Literal["baseline", "threshold", "provenance", "rollback"] artifact_id: str content_sha256: str provenance_uri: str created_at: datetime class HumanApprovalView(BaseModel): approval_event_id: UUID target_kind: Literal["content_qualification", "model_change_gate", "release_gate"] target_id: UUID decision: Literal[ "approve_content", "approve_promotion", "authorize_rollback", "reject", "keep_quarantine", ] reason_code: str evidence_refs: list[str] created_at: datetime class CatalogEntryView(BaseModel): catalog_record_id: UUID qualification_id: UUID catalog_entry_id: str status: Literal["approved"] clinical_claim_allowed: Literal[False] created_at: datetime class ApprovedCatalogConsumerEntry(BaseModel): catalog_record_id: UUID qualification_id: UUID catalog_entry_id: str payload_sha256: str content_kind: Literal["case", "rupture", "practice", "benchmark"] difficulty_level: int = Field(ge=1, le=5) synthetic_identity_id: str source_provenance_uris: list[str] = Field(min_length=1) payload: CatalogVisiblePayload status: Literal["approved"] clinical_claim_allowed: Literal[False] approved_at: datetime class ApprovedCatalogConsumerResponse(BaseModel): entries: list[ApprovedCatalogConsumerEntry] data_classification: SyntheticDataClassification human_approval_required: Literal[True] = True raw_transcript_included: Literal[False] = False pii_included: Literal[False] = False clinical_claim_allowed: Literal[False] = False class LifecycleEventView(BaseModel): lifecycle_event_id: UUID target_kind: Literal["model_change_gate", "release_gate"] target_id: UUID event_type: Literal["promotion", "rollback", "monitor"] event_status: Literal[ "approved", "requested", "executed", "failed", "healthy", "drift_detected", "rollback_recommended", "rollback_verified", ] approval_event_id: UUID | None = None artifact_record_id: UUID | None = None evidence_refs: list[str] executor_receipt_id: str | None = None executor_evidence_refs: list[str] | None = None created_at: datetime @model_validator(mode="after") def enforce_rollback_receipt_boundary(self) -> "LifecycleEventView": if self.event_type != "rollback": if self.executor_receipt_id is not None or self.executor_evidence_refs: raise ValueError("non-rollback lifecycle event cannot carry a receipt") return self if self.approval_event_id is None or self.artifact_record_id is None: raise ValueError("rollback requires approval and pinned artifact") if self.event_status == "executed": if not (self.executor_receipt_id or "").strip(): raise ValueError("executed rollback requires executor receipt id") if not self.executor_evidence_refs: raise ValueError("executed rollback requires executor evidence") if not set(self.executor_evidence_refs).issubset(self.evidence_refs): raise ValueError("executor evidence must be included in lifecycle evidence") elif self.executor_receipt_id is not None or self.executor_evidence_refs: raise ValueError("non-executed rollback cannot carry executor receipt evidence") return self class OperationalIncidentView(BaseModel): incident_record_id: UUID incident_id: str error_fingerprint: str affected_contract: str evidence_refs: list[str] pii_included: Literal[False] created_at: datetime class RegressionDagNodeView(BaseModel): node_record_id: UUID incident_record_id: UUID node_id: str node_type: Literal["reproduction_test", "implementation", "e2e", "runtime_proof"] depends_on_record_ids: list[UUID] evidence_ref: str | None = None node_status: Literal["pending", "passed", "failed"] created_at: datetime class ContinuousImprovementViewResponse(BaseModel): model_config = ConfigDict(protected_namespaces=()) content_qualifications: list[ContentQualificationView] model_change_gates: list[ModelChangeGateView] release_gates: list[ReleaseGateView] gate_artifacts: list[GateArtifactView] approvals: list[HumanApprovalView] catalog_entries: list[CatalogEntryView] lifecycle_events: list[LifecycleEventView] incidents: list[OperationalIncidentView] regression_dag_nodes: list[RegressionDagNodeView] data_classification: SyntheticDataClassification silent_auto_promotion_allowed: Literal[False] raw_transcript_included: Literal[False] pii_included: Literal[False] clinical_claim_allowed: Literal[False] def _artifact_dict(value: GateArtifact) -> dict[str, object]: return value.model_dump(mode="python") def _raise_store_error(exc: Exception) -> None: if isinstance(exc, continuous_improvement_store.ContinuousImprovementConflictError): raise HTTPException(status_code=409, detail=str(exc)) from exc if isinstance(exc, continuous_improvement_store.ContinuousImprovementNotFoundError): raise HTTPException(status_code=404, detail=str(exc)) from exc raise HTTPException(status_code=422, detail=str(exc)) from exc @router.post( "/internal/continuous-improvement/agentic-content-pipelines", response_model=AgenticContentPipelineResponse, status_code=201, ) async def create_agentic_content_pipeline( request: AgenticContentPipelineRequest, conn: ResearchDB ) -> AgenticContentPipelineResponse: """Run real model-owned generation/review/judging before pending approval.""" try: result = await continuous_improvement_agentic.run_agentic_content_pipeline( conn=conn, engine=engine_client, submission_id=request.submission_id, pipeline_id=request.pipeline_id, benchmark_record_id=request.benchmark_record_id, qualification_id=request.qualification_id, source_packs=request.source_packs, content_kind=request.content_kind, difficulty_level=request.difficulty_level, variant_count=request.variant_count, prompt_version=request.prompt_version, trigger_kind="source_pack", ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) except continuous_improvement_agentic.AgenticPipelineRejectedError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc except continuous_improvement_agentic.AgenticPipelineExecutionError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc return AgenticContentPipelineResponse.model_validate(result.model_dump(mode="json")) @router.post( "/internal/continuous-improvement/incidents/{incident_record_id}/adversarial-content-pipelines", response_model=AgenticContentPipelineResponse, status_code=201, ) async def create_incident_adversarial_content_pipeline( incident_record_id: UUID, request: IncidentAdversarialPipelineRequest, conn: ResearchDB, ) -> AgenticContentPipelineResponse: """Turn a persisted metadata-only operational failure into a gated benchmark.""" try: incident = await continuous_improvement_store.read_operational_incident( conn, incident_record_id=incident_record_id ) source_pack = ( continuous_improvement_agentic.source_pack_from_operational_incident( incident ) ) result = await continuous_improvement_agentic.run_agentic_content_pipeline( conn=conn, engine=engine_client, submission_id=request.submission_id, pipeline_id=request.pipeline_id, benchmark_record_id=request.benchmark_record_id, qualification_id=request.qualification_id, source_packs=[source_pack], content_kind="benchmark", difficulty_level=request.difficulty_level, variant_count=request.variant_count, prompt_version=request.prompt_version, trigger_kind="operational_incident", ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) except continuous_improvement_agentic.AgenticPipelineRejectedError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc except continuous_improvement_agentic.AgenticPipelineExecutionError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc return AgenticContentPipelineResponse.model_validate(result.model_dump(mode="json")) @router.post( "/internal/continuous-improvement/content-pipelines", response_model=ContentPipelineResponse, status_code=201, ) async def create_content_pipeline( request: ContentPipelineRequest, conn: ResearchDB ) -> ContentPipelineResponse: try: result = await continuous_improvement_store.submit_content_pipeline( conn, submission_id=request.submission_id, pipeline_id=request.pipeline_id, benchmark_record_id=request.benchmark_record_id, qualification_id=request.qualification_id, draft=request.draft, sources=request.sources, reviews=request.reviews, benchmark=request.benchmark, ) except (ValueError, continuous_improvement_store.ContinuousImprovementError) as exc: _raise_store_error(exc) return ContentPipelineResponse.model_validate(result) @router.post( "/internal/continuous-improvement/model-change-gates", response_model=ModelChangeGateResponse, status_code=201, ) async def create_model_change_gate( request: ModelChangeGateRequest, conn: ResearchDB ) -> ModelChangeGateResponse: try: result = await continuous_improvement_store.submit_model_change_gate( conn, submission_id=request.submission_id, gate_id=request.gate_id, baseline_snapshot_record_id=request.baseline_snapshot_record_id, candidate_snapshot_record_id=request.candidate_snapshot_record_id, baseline=request.baseline, candidate=request.candidate, baseline_artifact=_artifact_dict(request.artifacts.baseline), threshold_artifact=_artifact_dict(request.artifacts.threshold), provenance_artifacts=[ _artifact_dict(item) for item in request.artifacts.provenance ], rollback_artifact=_artifact_dict(request.artifacts.rollback), ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) return ModelChangeGateResponse.model_validate(result) @router.post( "/internal/continuous-improvement/release-gates", response_model=ReleaseGateResponse, status_code=201, ) async def create_release_gate( request: ReleaseGateRequest, conn: ResearchDB ) -> ReleaseGateResponse: try: result = await continuous_improvement_store.submit_release_gate( conn, submission_id=request.submission_id, gate_id=request.gate_id, manifest=request.manifest, baseline_artifact=_artifact_dict(request.artifacts.baseline), threshold_artifact=_artifact_dict(request.artifacts.threshold), provenance_artifacts=[ _artifact_dict(item) for item in request.artifacts.provenance ], rollback_artifact=_artifact_dict(request.artifacts.rollback), ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) return ReleaseGateResponse.model_validate(result) @router.post( "/internal/continuous-improvement/incidents", response_model=IncidentDagResponse, status_code=201, ) async def create_incident_dag( request: IncidentDagRequest, conn: ResearchDB ) -> IncidentDagResponse: try: result = await continuous_improvement_store.submit_incident_dag( conn, submission_id=request.submission_id, incident_record_id=request.incident_record_id, incident=request.incident, ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) return IncidentDagResponse.model_validate(result) @router.post( "/continuous-improvement/approvals", response_model=HumanApprovalResponse, status_code=201, ) async def create_human_approval( request: HumanApprovalRequest, principal: AdminPrincipal, conn: AdminDB, settings: Annotated[Settings, Depends(get_settings)], ) -> HumanApprovalResponse: try: rollback_executor = ( continuous_improvement_agentic.build_configured_rollback_executor(settings) if request.decision == "authorize_rollback" else None ) result = await continuous_improvement_store.append_human_approval( conn, submission_id=request.submission_id, approval_event_id=request.approval_event_id, effect_record_id=request.effect_record_id, target_kind=request.target_kind, target_id=request.target_id, decision=request.decision, actor_uid=UUID(principal.user_id), reason_code=request.reason_code, evidence_refs=request.evidence_refs, rollback_executor=rollback_executor, ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) return HumanApprovalResponse.model_validate(result) @router.post( "/internal/continuous-improvement/monitor-events", response_model=MonitorEventResponse, status_code=201, ) async def create_monitor_event( request: MonitorEventRequest, conn: ResearchDB ) -> MonitorEventResponse: try: result = await continuous_improvement_store.append_monitor_event( conn, submission_id=request.submission_id, lifecycle_event_id=request.lifecycle_event_id, target_kind=request.target_kind, target_id=request.target_id, event_status=request.event_status, evidence_refs=request.evidence_refs, ) except continuous_improvement_store.ContinuousImprovementError as exc: _raise_store_error(exc) return MonitorEventResponse.model_validate(result) @router.get( "/internal/continuous-improvement", response_model=ContinuousImprovementViewResponse, ) async def read_internal_continuous_improvement( conn: ResearchDB, ) -> ContinuousImprovementViewResponse: result = await continuous_improvement_store.read_continuous_improvement_view(conn) return ContinuousImprovementViewResponse.model_validate(result) @router.get( "/continuous-improvement", response_model=ContinuousImprovementViewResponse, ) async def read_admin_continuous_improvement( _principal: AdminPrincipal, conn: AdminDB, ) -> ContinuousImprovementViewResponse: result = await continuous_improvement_store.read_continuous_improvement_view(conn) return ContinuousImprovementViewResponse.model_validate(result) @router.get( "/continuous-improvement/catalog", response_model=ApprovedCatalogConsumerResponse, ) async def read_admin_approved_catalog( _principal: AdminPrincipal, conn: AdminDB, ) -> ApprovedCatalogConsumerResponse: entries = await continuous_improvement_store.read_approved_catalog_entries(conn) return ApprovedCatalogConsumerResponse( entries=[ApprovedCatalogConsumerEntry.model_validate(item) for item in entries], data_classification="synthetic_replay_red_team_coverage_drift", ) __all__ = [ "INTERNAL_TOKEN_HEADER", "continuous_improvement_internal_db", "continuous_improvement_admin_db", "router", ]