from __future__ import annotations import asyncio import logging from sqlmodel import Session, select from ..db import session as db_session from ..db.models import Policy from .audit_logger import log_event from .gitea_project_auto_registration import GiteaProjectAutoRegistration from .inventory_discovery import run_discovery from .mcp_hub_deployment_reconciliation import McpHubDeploymentReconciliation from .media_codec_dashboard_deployment_reconciliation import ( MediaCodecDashboardDeploymentReconciliation, ) from .privatemuse_deployment_enrollment import PrivateMuseDeploymentEnrollment logger = logging.getLogger(__name__) _MCP_HUB_APPLICATION_ID = "itworx-mcp-hub" _MCP_HUB_REPOSITORY = "jens/itworx_mcp_hub" _MCP_HUB_DEPLOYMENT_ACTIONS = ("deployment.apply", "deployment.rollback") def _reconcile_mcp_hub_exact_policies(session: Session) -> dict[str, object]: """Create only the exact MCP Hub deployment policies after state proof.""" existing_by_action: dict[str, Policy | None] = {} for action in _MCP_HUB_DEPLOYMENT_ACTIONS: matches = session.exec( select(Policy).where( (Policy.action == action) & (Policy.target == _MCP_HUB_APPLICATION_ID) ) ).all() if len(matches) > 1: return { "status": "conflict", "action": action, "created": 0, "policyIds": [], } existing = matches[0] if matches else None if existing is not None and ( existing.manual != "allowed" or existing.voice != "blocked" or existing.risk != "high" ): return { "status": "conflict", "action": action, "created": 0, "policyIds": [], } existing_by_action[action] = existing policy_ids: list[str] = [] created = 0 for action in _MCP_HUB_DEPLOYMENT_ACTIONS: existing = existing_by_action[action] if existing is not None: policy_ids.append(existing.id) continue suffix = action.rsplit(".", 1)[-1] policy_id = f"mcp-hub-reconciled-{suffix}" if session.get(Policy, policy_id) is not None: return { "status": "conflict", "action": action, "created": 0, "policyIds": [], } session.add( Policy( id=policy_id, action=action, target=_MCP_HUB_APPLICATION_ID, voice="blocked", manual="allowed", risk="high", reason=( "Exact MCP Hub deployment policy created only after " "fail-closed runtime state reconciliation." ), provenance_json={ "createdBy": "mcp-hub-state-reconciliation/v1", "repository": _MCP_HUB_REPOSITORY, "policyTemplateVersion": "exact-target/v1", }, ) ) policy_ids.append(policy_id) created += 1 session.commit() return {"status": "ready", "created": created, "policyIds": policy_ids} def _ensure_mcp_hub_exact_policies() -> dict[str, object]: with Session(db_session.engine) as session: return _reconcile_mcp_hub_exact_policies(session) def _audit_privatemuse_enrollment(enrollment: dict[str, object]) -> None: status = str(enrollment.get("status") or "error") failure_reason = str(enrollment.get("reason") or "") reason = "Bounded startup reconciliation for the existing PrivateMuse deployment target." if failure_reason: reason = f"{reason} Enrollment result: {status}; {failure_reason}" with Session(db_session.engine) as session: log_event( session, source="system", identity="appops-startup", action_id="deployment.target.enroll", target="jens/privatemuse", target_type="deployment-target", risk="high", policy_decision="allowed", reason=reason, result="success" if status == "enrolled" else "failed", after_state={ "status": status, "repository": enrollment.get("repository", "jens/privatemuse"), "legacyRevision": enrollment.get("legacy_revision", ""), "composeProject": enrollment.get("compose_project", ""), "containers": enrollment.get("containers", []), }, redacted_metadata={"failureReason": failure_reason} if failure_reason else None, ) def _audit_mcp_hub_reconciliation(result: dict[str, object]) -> None: status = str(result.get("status") or "error") failure_reason = str(result.get("reason") or "") reason = "Fail-closed startup reconciliation for the verified MCP Hub deployment state." if failure_reason: reason = f"{reason} Reconciliation result: {status}; {failure_reason}" with Session(db_session.engine) as session: log_event( session, source="system", identity="appops-startup", action_id="deployment.state.reconcile", target="jens/itworx_mcp_hub", target_type="deployment-target", risk="high", policy_decision="allowed", reason=reason, result="success" if status == "reconciled" else "failed", after_state={ "status": status, "repository": result.get("repository", "jens/itworx_mcp_hub"), "composeProject": result.get("composeProject", "itworx-mcp-hub"), "policyStatus": result.get("policyStatus", "not_checked"), "policyIds": result.get("policyIds", []), "policiesCreated": result.get("policiesCreated", 0), }, redacted_metadata={"failureReason": failure_reason} if failure_reason else None, ) def _audit_media_codec_dashboard_reconciliation(result: dict[str, object]) -> None: status = str(result.get("status") or "error") failure_reason = str(result.get("reason") or "") reason = "Fail-closed startup reconciliation for the verified Media Codec Dashboard deployment state." if failure_reason: reason = f"{reason} Reconciliation result: {status}; {failure_reason}" with Session(db_session.engine) as session: log_event( session, source="system", identity="appops-startup", action_id="deployment.state.reconcile", target="jens/media-codec-dashboard", target_type="deployment-target", risk="high", policy_decision="allowed", reason=reason, result="success" if status == "reconciled" else "failed", after_state={ "status": status, "repository": result.get("repository", "jens/media-codec-dashboard"), "liveRevision": result.get("live_revision", ""), "previousRecordedRevision": result.get("previous_recorded_revision", ""), "container": result.get("container", ""), }, redacted_metadata={"failureReason": failure_reason} if failure_reason else None, ) def _sync_gitea_projects() -> dict[str, object]: with Session(db_session.engine) as session: result = GiteaProjectAutoRegistration().sync(session) summary = result.get("summary") if isinstance(result.get("summary"), dict) else {} changed = int(summary.get("changedThisSync", 0) or 0) if changed: log_event( session, source="system", identity="appops-project-sync", action_id="deployment.targets.reconcile", target="jens/*", target_type="deployment-project-catalog", risk="medium", policy_decision="allowed", reason="Automatic Gitea project registration reconciled changed owned repositories.", result="success" if result.get("status") == "ok" else "failed", after_state={ "status": result.get("status"), "total": summary.get("total", 0), "registered": summary.get("registered", 0), "knownUnconfigured": summary.get("knownUnconfigured", 0), "conflicts": summary.get("conflicts", 0), "errors": summary.get("errors", 0), "changed": changed, }, ) return result async def _run_gitea_project_sync() -> None: try: result = await asyncio.to_thread(_sync_gitea_projects) summary = result.get("summary") if isinstance(result.get("summary"), dict) else {} if result.get("status") != "ok": logger.warning( "Gitea project registration sync degraded: status=%s reason=%s", result.get("status"), result.get("reason", "unspecified"), ) else: logger.info( "Gitea project registration sync complete: total=%s registered=%s changed=%s", summary.get("total", 0), summary.get("registered", 0), summary.get("changedThisSync", 0), ) except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - catalog sync must never take down AppOps. logger.exception("Gitea project registration sync failed") async def run_periodic_discovery(interval_seconds: int) -> None: """Reconcile deployment project catalog, bounded target state and inventory.""" await _run_gitea_project_sync() try: enrollment = await asyncio.to_thread(PrivateMuseDeploymentEnrollment().run) await asyncio.to_thread(_audit_privatemuse_enrollment, enrollment) if enrollment.get("status") != "enrolled": logger.warning( "PrivateMuse deployment enrollment did not complete: status=%s reason=%s", enrollment.get("status"), enrollment.get("reason", "unspecified"), ) else: logger.info("PrivateMuse deployment target is enrolled") except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - enrollment failure must not take down AppOps. logger.exception("PrivateMuse deployment enrollment failed") try: reconciliation = await asyncio.to_thread(McpHubDeploymentReconciliation().run) if reconciliation.get("status") == "reconciled": policy_result = await asyncio.to_thread(_ensure_mcp_hub_exact_policies) reconciliation = { **reconciliation, "policyStatus": policy_result.get("status", "error"), "policyIds": policy_result.get("policyIds", []), "policiesCreated": policy_result.get("created", 0), } if policy_result.get("status") != "ready": reconciliation = { **reconciliation, "status": "policy_conflict", "reason": "exact MCP Hub deployment policy reconciliation failed closed", } await asyncio.to_thread(_audit_mcp_hub_reconciliation, reconciliation) if reconciliation.get("status") != "reconciled": logger.warning( "MCP Hub deployment reconciliation did not complete: status=%s reason=%s", reconciliation.get("status"), reconciliation.get("reason", "unspecified"), ) else: logger.info("MCP Hub deployment state is reconciled") except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - reconciliation failure must not take down AppOps. logger.exception("MCP Hub deployment reconciliation failed") try: reconciliation = await asyncio.to_thread( MediaCodecDashboardDeploymentReconciliation().run ) await asyncio.to_thread( _audit_media_codec_dashboard_reconciliation, reconciliation ) if reconciliation.get("status") != "reconciled": logger.warning( "Media Codec Dashboard deployment reconciliation did not complete: status=%s reason=%s", reconciliation.get("status"), reconciliation.get("reason", "unspecified"), ) else: logger.info("Media Codec Dashboard deployment state is reconciled") except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - reconciliation failure must not take down AppOps. logger.exception("Media Codec Dashboard deployment reconciliation failed") if interval_seconds <= 0: return await asyncio.sleep(min(10, interval_seconds)) while True: try: await asyncio.to_thread(run_discovery) except asyncio.CancelledError: raise except Exception: # noqa: BLE001 - discovery must never take down the gateway. logger.exception("Scheduled inventory discovery failed") await _run_gitea_project_sync() await asyncio.sleep(interval_seconds)