"""Fixed operator-only coordinator for the one RepoPulse adoption. AppOps prepares and reserves the fixed intent, then finalizes only evidence that has been collected by the separately operated root controller. This module never asks the controller to execute a cutover and exposes no API route, Portainer operation, caller-selected path, or arbitrary command. """ from __future__ import annotations import argparse import hashlib import json import re import sys from typing import Any from urllib.parse import urlparse from sqlalchemy.dialects.sqlite import insert as sqlite_insert from sqlalchemy.exc import SQLAlchemyError from sqlmodel import Session from ..core.time import utc_now from ..db.models import AppSetting from ..db.session import engine from ..schemas.enrollment import EnrollmentTrustConfigV1 from . import ( repopulse_adoption_journal as journal, ) from . import ( repopulse_adoption_ownership as ownership, ) from . import ( repopulse_legacy_adoption_protocol as protocol, ) from . import ( secret_store, ) from .deployment_broker import DeploymentBrokerClient from .enrollment_plane import TRUST_SETTING_KEY from .repopulse_adoption_preflight import RepoPulseAdoptionPreflight from .repopulse_target_credential import ( RepoPulseTargetCredentialError, get_or_create_repopulse_target_credential, ) from .setup_service import apply_stored_connector_settings _HANDOFF_KEY = "repopulse_adoption:controller_handoff" _BOOTSTRAP_SECRET_MINIMUM = 32 _SHA256 = re.compile(r"^[0-9a-f]{64}$") _IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$") _CAPABILITIES_VERSION = "itworx.repopulse-adoption-capabilities/v1" _RESERVE_VERSION = "itworx.repopulse-adoption-reserve/v1" _STATUS_VERSION = "itworx.repopulse-adoption-status/v1" _EXECUTION_RESULT_VERSION = "itworx.repopulse-controller-adoption-execution-result/v1" _REGISTRY_RECEIPT_VERSION = "itworx.repopulse-controller-adoption-store-receipt/v1" _COMPLETION_VERSION = "itworx.repopulse-controller-adoption-completion/v1" _LEGACY_VOLUME = "repopulse_data" _MAX_CONTROLLER_RESULT_BYTES = 256 * 1024 _RECOVERY_STATUS_PHASES = { "reserved", "target_gates_ready", "quiesced", "snapshot_verified", } _COPY_KEYS = { "intentId", "snapshotFileSha256", "targetLogicalSha256", "targetScanCount", "targetCompactedScanCount", "targetQueueSha256", "sourceAuxiliaryTreeSha256", "targetAuxiliaryTreeSha256", "sourceAuxiliaryFileCount", "targetAuxiliaryFileCount", "targetQuickCheck", "targetIntegrityCheck", "targetDataUid", "targetDataGid", "targetRecoveryStatus", "managedDataMountMatches", "copySourceIsVerifiedSnapshot", "legacyVolumeRetained", "legacyEnvironmentRetained", "managedEnvironmentFilePrivate", } _DEPLOYMENT_RECEIPT_KEYS = { "repository", "revision", "containerRevision", "containerIdSha256", "deploymentReceiptSchema", "deploymentReceiptSha256", "deploymentReceiptVerified", "runtimeHealthy", "publishedHostPorts", "managedDataMountMatches", "targetDataUid", "targetDataGid", "targetLogicalSha256", "targetScanCount", "targetCompactedScanCount", "targetQueueSha256", "targetAuxiliaryTreeSha256", "targetAuxiliaryFileCount", "targetQuickCheck", "targetIntegrityCheck", "targetRecoveryStatus", "targetQueuedJobs", "targetActiveJobs", "legacyVolume", "legacyVolumeRetained", "legacyEnvironmentRetained", "managedEnvironmentFilePrivate", "scanSourceMatches", "scanMountReadOnly", "scanPermissionPolicyPersistent", "scanAccessForRuntimeIdentityReady", "containerName", "containerImageSha256", "snapshotFileSha256", } _REGISTRY_RECEIPT_KEYS = { "apiVersion", "intentId", "repository", "applicationId", "phase", "intentBindingSha256", "legacyVolumeRetained", "intentDurable", "managedEnrollmentId", "projectsSha256", "snapshotFileSha256", "receiptSha256", } _COMPLETION_KEYS = { "apiVersion", "intentId", "intentBindingSha256", "controllerRegistrationReceiptSha256", "mainRevision", "deploymentReceiptSha256", "snapshotFileSha256", "snapshotLogicalSha256", "snapshotQueueSha256", "auxiliaryTreeSha256", "containerIdSha256", "containerImageSha256", "legacyVolume", "legacyVolumeRetained", "completionSha256", } _RESULT_KEYS = { "apiVersion", "repository", "applicationId", "intentId", "status", "phase", "legacyVolume", "legacyVolumeRetained", "targetGates", "exactShaValidation", "quiescence", "snapshot", "copy", "registryReceipt", "deploymentReceipt", "completion", } _STATUS_HASH_KEYS = { "intentSha256", "intentBindingSha256", "idempotencySha256", "preflightSha256", "expectedProjectsSha256", "quiescenceSha256", "planSha256", "snapshotFileSha256", "snapshotLogicalSha256", "snapshotQueueSha256", "targetProjectsSha256", "receiptSha256", } class RepoPulseAdoptionRunnerError(ValueError): """Sanitized fixed-runner failure.""" def __init__(self, code: str): super().__init__(code) self.code = code def _need(condition: bool, code: str) -> None: if not condition: raise RepoPulseAdoptionRunnerError(code) def _sha(value: object) -> bool: return isinstance(value, str) and _SHA256.fullmatch(value) is not None def _count(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) and value >= 0 def _canonical_sha(value: object) -> str: try: encoded = json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False, ).encode("ascii") except (TypeError, ValueError, UnicodeEncodeError): raise RepoPulseAdoptionRunnerError("controller_evidence_encoding_invalid") from None return hashlib.sha256(encoded).hexdigest() def _controller_document_sha(value: object) -> str: try: encoded = ( json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False, ) + "\n" ).encode("ascii") except (TypeError, ValueError, UnicodeEncodeError): raise RepoPulseAdoptionRunnerError("controller_evidence_encoding_invalid") from None return hashlib.sha256(encoded).hexdigest() def _safe_error_code(value: object, fallback: str) -> str: code = getattr(value, "code", None) if isinstance(code, str) and re.fullmatch(r"[a-z0-9_]{1,64}", code): return code return fallback def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: result: dict[str, Any] = {} for key, value in pairs: if key in result: raise ValueError("duplicate_json_key") result[key] = value return result def _reject_json_constant(_: str) -> None: raise ValueError("non_finite_json_number") def _read_controller_result_stdin() -> object: stream = getattr(sys.stdin, "buffer", None) if stream is None: raw = sys.stdin.read(_MAX_CONTROLLER_RESULT_BYTES + 1).encode("utf-8") else: raw = stream.read(_MAX_CONTROLLER_RESULT_BYTES + 1) _need( isinstance(raw, bytes) and 0 < len(raw) <= _MAX_CONTROLLER_RESULT_BYTES, "controller_result_input_invalid", ) try: return json.loads( raw.decode("utf-8"), object_pairs_hook=_unique_json_object, parse_constant=_reject_json_constant, ) except (UnicodeDecodeError, json.JSONDecodeError, ValueError): raise RepoPulseAdoptionRunnerError("controller_result_input_invalid") from None def _safe_status(status: dict[str, Any], *, reason: str | None = None) -> dict[str, Any]: result = { key: status.get(key) for key in ("status", "phase", "revision", "intentId") if key in status } if reason is not None: result["reason"] = reason elif status.get("status") == "recovery_required" and isinstance( status.get("recoveryReason"), str ): result["reason"] = status["recoveryReason"] return result def _trust_and_bootstrap(session: Session) -> tuple[EnrollmentTrustConfigV1, str]: row = session.get(AppSetting, TRUST_SETTING_KEY) _need(row is not None and row.is_secret is False, "enrollment_trust_unavailable") try: trust = EnrollmentTrustConfigV1.model_validate(row.value_json) except (TypeError, ValueError): raise RepoPulseAdoptionRunnerError("enrollment_trust_invalid") from None _need( trust.controller_identity == "unraid-controller" and "repopulse-authenticated" in trust.autonomous_profiles and "itworx-mcp-hub-npm" in trust.approved_networks and "example.com" in trust.allowed_domain_suffixes and trust.repopulse_proxy_access_list_id is not None, "repopulse_trust_boundary_mismatch", ) value = secret_store.get_secret(session, trust.bootstrap_secret_ref, touch=False) _need( isinstance(value, str) and len(value) >= _BOOTSTRAP_SECRET_MINIMUM and value.isascii() and value.isprintable(), "controller_bootstrap_secret_unavailable", ) return trust, value def _validate_transport(broker: DeploymentBrokerClient) -> None: parsed = urlparse(str(getattr(broker, "base_url", ""))) _need( parsed.scheme == "https" and bool(parsed.netloc) and getattr(broker, "verify_tls", False) is True and broker.configuration_error() is None, "controller_private_tls_required", ) def _validate_capabilities(value: object) -> None: expected = { "apiVersion", "status", "repository", "adoptionStore", "privateTls", "controller", } _need(isinstance(value, dict) and set(value) == expected, "controller_capabilities_invalid") _need( value.get("apiVersion") == _CAPABILITIES_VERSION and value.get("status") == "ready" and value.get("repository") == protocol.REPOSITORY and value.get("adoptionStore") == {"executionEnabled": True} and value.get("privateTls") == {"status": "ready"} and value.get("controller") == {"aliases": "ready", "registry": "ready", "secretRoot": "ready"}, "controller_capabilities_not_ready", ) def _validate_reservation(value: object, intent: dict[str, Any]) -> str: expected = { "apiVersion", "status", "intentId", "phase", "intentSha256", "intentBindingSha256", } _need(isinstance(value, dict) and set(value) == expected, "controller_reservation_invalid") binding = protocol.intent_binding_sha256(intent) _need( value.get("apiVersion") == _RESERVE_VERSION and value.get("status") == "reserved" and value.get("intentId") == intent["intentId"] and value.get("phase") == "reserved" and _sha(value.get("intentSha256")) and value.get("intentBindingSha256") == binding, "controller_reservation_unbound", ) return value["intentSha256"] def _read_handoff(session: Session) -> dict[str, Any] | None: row = session.get(AppSetting, _HANDOFF_KEY, populate_existing=True) if row is None: return None expected = { "intentId", "idempotencySha256", "preflightSha256", "intentBindingSha256", "controllerIntentSha256", "state", } value = row.value_json if ( row.is_secret is not False or not isinstance(value, dict) or set(value) != expected or value.get("state") not in { "reserve_attempted", "handoff_prepared", "controller_cli_handoff_issued", } or not isinstance(value.get("intentId"), str) or not _sha(value.get("idempotencySha256")) or not _sha(value.get("preflightSha256")) or not _sha(value.get("intentBindingSha256")) or ( value.get("state") == "reserve_attempted" and value.get("controllerIntentSha256") is not None ) or ( value.get("state") in {"handoff_prepared", "controller_cli_handoff_issued"} and not _sha(value.get("controllerIntentSha256")) ) ): raise RepoPulseAdoptionRunnerError("controller_handoff_marker_invalid") return value def _handoff_record( intent: dict[str, Any], state: str, controller_intent_sha256: str | None ) -> dict[str, Any]: return { "intentId": intent["intentId"], "idempotencySha256": intent["idempotencySha256"], "preflightSha256": intent["preflightSha256"], "intentBindingSha256": protocol.intent_binding_sha256(intent), "controllerIntentSha256": controller_intent_sha256, "state": state, } def _persist_reserve_attempt(session: Session, intent: dict[str, Any]) -> dict[str, Any]: """Persist the one fixed reserve attempt before its external POST.""" value = _handoff_record(intent, "reserve_attempted", None) statement = sqlite_insert(AppSetting).values( key=_HANDOFF_KEY, value_json=value, is_secret=False, updated_at=utc_now(), ) try: session.execute(statement.on_conflict_do_nothing(index_elements=[AppSetting.key])) session.commit() existing = _read_handoff(session) except SQLAlchemyError: session.rollback() raise RepoPulseAdoptionRunnerError("controller_handoff_marker_unavailable") from None _need( existing is not None and _handoff_matches(existing, intent) and existing.get("state") in {"reserve_attempted", "handoff_prepared", "controller_cli_handoff_issued"}, "controller_handoff_marker_conflict", ) return existing def _persist_handoff( session: Session, intent: dict[str, Any], controller_intent_sha256: str ) -> dict[str, Any]: value = _handoff_record(intent, "handoff_prepared", controller_intent_sha256) try: current = _read_handoff(session) if current is None: session.execute( sqlite_insert(AppSetting) .values( key=_HANDOFF_KEY, value_json=value, is_secret=False, updated_at=utc_now(), ) .on_conflict_do_nothing(index_elements=[AppSetting.key]) ) elif _handoff_matches(current, intent): _need( current.get("state") in {"reserve_attempted", "controller_cli_handoff_issued"} or current.get("controllerIntentSha256") == controller_intent_sha256, "controller_handoff_marker_conflict", ) if current.get("state") == "controller_cli_handoff_issued": _need( current.get("controllerIntentSha256") == controller_intent_sha256, "controller_handoff_marker_conflict", ) value = current else: session.execute( AppSetting.__table__.update() .where(AppSetting.key == _HANDOFF_KEY) .values(value_json=value, is_secret=False, updated_at=utc_now()) ) else: raise RepoPulseAdoptionRunnerError("controller_handoff_marker_conflict") session.commit() existing = _read_handoff(session) except SQLAlchemyError: session.rollback() raise RepoPulseAdoptionRunnerError("controller_handoff_marker_unavailable") from None _need(existing == value, "controller_handoff_marker_conflict") return value def _persist_cli_handoff_issued( session: Session, intent: dict[str, Any], controller_intent_sha256: str ) -> dict[str, Any]: """Consume the one secret-bearing pipe handoff before returning it.""" current = _read_handoff(session) _need( current is not None and _handoff_matches(current, intent) and current.get("state") == "handoff_prepared" and current.get("controllerIntentSha256") == controller_intent_sha256, "controller_cli_handoff_replay_requires_recovery", ) value = _handoff_record( intent, "controller_cli_handoff_issued", controller_intent_sha256 ) try: result = session.execute( AppSetting.__table__.update() .where( AppSetting.key == _HANDOFF_KEY, AppSetting.value_json == current, ) .values(value_json=value, is_secret=False, updated_at=utc_now()) ) _need(result.rowcount == 1, "controller_cli_handoff_replay_requires_recovery") session.commit() existing = _read_handoff(session) except SQLAlchemyError: session.rollback() raise RepoPulseAdoptionRunnerError("controller_cli_handoff_marker_unavailable") from None _need(existing == value, "controller_cli_handoff_marker_conflict") return value def _status_projection(value: object) -> dict[str, Any]: expected = {"apiVersion", "status", "phase", "hashes"} _need(isinstance(value, dict) and set(value) == expected, "controller_status_invalid") hashes = value.get("hashes") _need( value.get("apiVersion") == _STATUS_VERSION and isinstance(value.get("status"), str) and value.get("status") in {"in_progress", "recovery_required", "not_reserved"} and (value.get("phase") is None or value.get("phase") in _RECOVERY_STATUS_PHASES) and isinstance(hashes, dict) and set(hashes).issubset(_STATUS_HASH_KEYS) and all(item is None or _sha(item) for item in hashes.values()), "controller_status_invalid", ) if value["status"] == "not_reserved": _need(value.get("phase") is None and hashes == {}, "controller_status_invalid") else: _need(value.get("phase") in _RECOVERY_STATUS_PHASES, "controller_status_invalid") required = { "intentSha256", "intentBindingSha256", "idempotencySha256", "preflightSha256", "expectedProjectsSha256", } _need(required.issubset(hashes), "controller_status_invalid") return value def _status_failure_evidence(value: object) -> dict[str, Any]: try: checked = _status_projection(value) except RepoPulseAdoptionRunnerError: return {"controllerStatus": "unavailable"} return { "controllerStatus": checked["status"], "controllerPhase": checked["phase"], "statusHashes": checked["hashes"], } def _validate_registry_receipt(value: object, intent: dict[str, Any]) -> dict[str, Any]: _need(isinstance(value, dict) and set(value) == _REGISTRY_RECEIPT_KEYS, "controller_registry_receipt_invalid") receipt = dict(value) digest = receipt.pop("receiptSha256") _need( receipt.get("apiVersion") == _REGISTRY_RECEIPT_VERSION and receipt.get("intentId") == intent["intentId"] and receipt.get("repository") == protocol.REPOSITORY and receipt.get("applicationId") == protocol.APPLICATION_ID and receipt.get("phase") == "registry_committed" and receipt.get("intentBindingSha256") == protocol.intent_binding_sha256(intent) and receipt.get("legacyVolumeRetained") is True and receipt.get("intentDurable") is True and receipt.get("managedEnrollmentId") == f"enr-{intent['intentId'][4:]}" and _sha(receipt.get("projectsSha256")) and _sha(receipt.get("snapshotFileSha256")) and _sha(digest) and digest == _controller_document_sha(receipt), "controller_registry_receipt_invalid", ) receipt["receiptSha256"] = digest return receipt def _validate_copy(value: object, snapshot: dict[str, Any], intent_id: str) -> dict[str, Any]: _need(isinstance(value, dict) and set(value) == _COPY_KEYS, "controller_copy_evidence_invalid") evidence = dict(value) _need( evidence.get("intentId") == intent_id and evidence.get("snapshotFileSha256") == snapshot["snapshotFileSha256"] and evidence.get("targetLogicalSha256") == snapshot["snapshotLogicalSha256"] and _count(evidence.get("targetScanCount")) and evidence.get("targetScanCount") == snapshot["snapshotScanCount"] and _count(evidence.get("targetCompactedScanCount")) and evidence.get("targetCompactedScanCount") == snapshot["snapshotCompactedScanCount"] and evidence.get("targetQueueSha256") == snapshot["snapshotQueueSha256"] and evidence.get("sourceAuxiliaryTreeSha256") == snapshot["sourceAuxiliaryTreeSha256"] and evidence.get("targetAuxiliaryTreeSha256") == snapshot["sourceAuxiliaryTreeSha256"] and _count(evidence.get("sourceAuxiliaryFileCount")) and evidence.get("sourceAuxiliaryFileCount") == snapshot["sourceAuxiliaryFileCount"] and _count(evidence.get("targetAuxiliaryFileCount")) and evidence.get("targetAuxiliaryFileCount") == snapshot["sourceAuxiliaryFileCount"] and evidence.get("targetQuickCheck") == "ok" and evidence.get("targetIntegrityCheck") == "ok" and evidence.get("targetDataUid") == 10001 and evidence.get("targetDataGid") == 10001 and evidence.get("targetRecoveryStatus") in {"healthy", "acknowledged"} and evidence.get("managedDataMountMatches") is True and evidence.get("copySourceIsVerifiedSnapshot") is True and evidence.get("legacyVolumeRetained") is True and evidence.get("legacyEnvironmentRetained") is True and evidence.get("managedEnvironmentFilePrivate") is True, "controller_copy_evidence_unverified", ) return evidence def _validate_deployment_receipt( value: object, intent: dict[str, Any], snapshot: dict[str, Any], copied: dict[str, Any] ) -> dict[str, Any]: _need(isinstance(value, dict) and set(value) == _DEPLOYMENT_RECEIPT_KEYS, "controller_deployment_receipt_invalid") receipt = dict(value) revision = intent["evidence"]["preflight"]["main_revision"] _need( receipt.get("repository") == protocol.REPOSITORY and receipt.get("revision") == revision and receipt.get("containerRevision") == revision and _sha(receipt.get("containerIdSha256")) and receipt.get("deploymentReceiptSchema") == "gitea-deploy-receipt.v1" and _sha(receipt.get("deploymentReceiptSha256")) and receipt.get("deploymentReceiptVerified") is True and receipt.get("runtimeHealthy") is True and receipt.get("publishedHostPorts") == [] and receipt.get("managedDataMountMatches") is True and receipt.get("targetDataUid") == copied["targetDataUid"] and receipt.get("targetDataGid") == copied["targetDataGid"] and receipt.get("targetLogicalSha256") == copied["targetLogicalSha256"] and receipt.get("targetScanCount") == copied["targetScanCount"] and receipt.get("targetCompactedScanCount") == copied["targetCompactedScanCount"] and receipt.get("targetQueueSha256") == copied["targetQueueSha256"] and receipt.get("targetAuxiliaryTreeSha256") == copied["targetAuxiliaryTreeSha256"] and receipt.get("targetAuxiliaryFileCount") == copied["targetAuxiliaryFileCount"] and receipt.get("targetQuickCheck") == copied["targetQuickCheck"] and receipt.get("targetIntegrityCheck") == copied["targetIntegrityCheck"] and receipt.get("targetRecoveryStatus") == copied["targetRecoveryStatus"] and receipt.get("targetQueuedJobs") == 0 and receipt.get("targetActiveJobs") == 0 and receipt.get("legacyVolume") == _LEGACY_VOLUME and receipt.get("legacyVolumeRetained") is True and receipt.get("legacyEnvironmentRetained") is True and receipt.get("managedEnvironmentFilePrivate") is True and receipt.get("scanSourceMatches") is True and receipt.get("scanMountReadOnly") is True and receipt.get("scanPermissionPolicyPersistent") is True and receipt.get("scanAccessForRuntimeIdentityReady") is True and receipt.get("containerName") == "repopulse" and _sha(receipt.get("containerImageSha256")) and receipt.get("snapshotFileSha256") == snapshot["snapshotFileSha256"], "controller_deployment_receipt_unverified", ) for key in ("targetScanCount", "targetCompactedScanCount", "targetAuxiliaryFileCount", "targetQueuedJobs", "targetActiveJobs"): _need(_count(receipt.get(key)), "controller_deployment_receipt_invalid") return receipt def _validate_completion( value: object, intent: dict[str, Any], snapshot: dict[str, Any], copied: dict[str, Any], registry: dict[str, Any], deployment: dict[str, Any], ) -> dict[str, Any]: _need(isinstance(value, dict) and set(value) == _COMPLETION_KEYS, "controller_completion_invalid") completion = dict(value) digest = completion.pop("completionSha256") _need( completion.get("apiVersion") == _COMPLETION_VERSION and completion.get("intentId") == intent["intentId"] and completion.get("intentBindingSha256") == protocol.intent_binding_sha256(intent) and completion.get("controllerRegistrationReceiptSha256") == registry["receiptSha256"] and completion.get("mainRevision") == intent["evidence"]["preflight"]["main_revision"] and completion.get("deploymentReceiptSha256") == deployment["deploymentReceiptSha256"] and completion.get("snapshotFileSha256") == snapshot["snapshotFileSha256"] and completion.get("snapshotLogicalSha256") == snapshot["snapshotLogicalSha256"] and completion.get("snapshotQueueSha256") == snapshot["snapshotQueueSha256"] and completion.get("auxiliaryTreeSha256") == copied["targetAuxiliaryTreeSha256"] and completion.get("containerIdSha256") == deployment["containerIdSha256"] and completion.get("containerImageSha256") == deployment["containerImageSha256"] and completion.get("legacyVolume") == _LEGACY_VOLUME and completion.get("legacyVolumeRetained") is True and _sha(digest) and digest == _canonical_sha(completion), "controller_completion_binding_invalid", ) completion["completionSha256"] = digest return completion def _map_controller_result( value: object, intent: dict[str, Any] ) -> tuple[dict[str, dict[str, Any]], str, str]: _need(isinstance(value, dict) and set(value) == _RESULT_KEYS, "controller_result_envelope_invalid") result = value _need( result.get("apiVersion") == _EXECUTION_RESULT_VERSION and result.get("repository") == protocol.REPOSITORY and result.get("applicationId") == protocol.APPLICATION_ID and result.get("intentId") == intent["intentId"] and result.get("status") == "deployment_verified" and result.get("phase") == "completion_verified" and result.get("legacyVolume") == _LEGACY_VOLUME and result.get("legacyVolumeRetained") is True, "controller_result_identity_invalid", ) gates = result.get("targetGates") quiescence = result.get("quiescence") snapshot = result.get("snapshot") _need(isinstance(gates, dict), "controller_target_gates_invalid") _need(isinstance(quiescence, dict), "controller_quiescence_invalid") _need(isinstance(snapshot, dict), "controller_snapshot_invalid") validation = result.get("exactShaValidation") _need( isinstance(validation, dict) and set(validation) == {"repository", "revision", "contexts"} and validation.get("repository") == protocol.REPOSITORY and validation.get("revision") == intent["evidence"]["preflight"]["main_revision"] and validation.get("contexts") == {"Managed validation / full (push)": "success"}, "controller_exact_sha_validation_invalid", ) checked_registry = _validate_registry_receipt(result.get("registryReceipt"), intent) _need( checked_registry["snapshotFileSha256"] == snapshot.get("snapshotFileSha256"), "controller_registry_snapshot_mismatch", ) copied = _validate_copy(result.get("copy"), snapshot, intent["intentId"]) deployment = _validate_deployment_receipt( result.get("deploymentReceipt"), intent, snapshot, copied ) _need( copied["managedDataMountMatches"] is True and deployment["managedDataMountMatches"] is True, "controller_data_mount_mismatch", ) completion = _validate_completion( result.get("completion"), intent, snapshot, copied, checked_registry, deployment, ) registration = { "receiptVersion": protocol.CONTROLLER_RECEIPT_VERSION, "intentId": intent["intentId"], # The completion receipt binds the registry, copied data, deployment, # and live container proof as one immutable controller result. "receiptSha256": completion["completionSha256"], "repository": protocol.REPOSITORY, "applicationId": protocol.APPLICATION_ID, "enrollmentId": checked_registry["managedEnrollmentId"], "profileId": gates.get("profileId"), "planSha256": gates.get("planSha256"), "snapshotFileSha256": snapshot.get("snapshotFileSha256"), "targetLogicalSha256": copied["targetLogicalSha256"], "targetScanCount": copied["targetScanCount"], "targetCompactedScanCount": copied["targetCompactedScanCount"], "targetQueueSha256": copied["targetQueueSha256"], "targetAuxiliaryTreeSha256": copied["targetAuxiliaryTreeSha256"], "targetAuxiliaryFileCount": copied["targetAuxiliaryFileCount"], "targetQueuedJobs": deployment["targetQueuedJobs"], "targetActiveJobs": deployment["targetActiveJobs"], "targetQuickCheck": copied["targetQuickCheck"], "targetIntegrityCheck": copied["targetIntegrityCheck"], "targetRecoveryStatus": copied["targetRecoveryStatus"], "targetDataUid": copied["targetDataUid"], "targetDataGid": copied["targetDataGid"], "managedDataMountMatches": deployment["managedDataMountMatches"], "copySourceIsVerifiedSnapshot": copied["copySourceIsVerifiedSnapshot"], "registryTargetMatches": ( checked_registry["managedEnrollmentId"] == f"enr-{intent['intentId'][4:]}" and checked_registry["snapshotFileSha256"] == snapshot["snapshotFileSha256"] ), "controllerStateReceiptVerified": checked_registry["receiptSha256"] == completion["controllerRegistrationReceiptSha256"], "deploymentReceiptSchema": deployment["deploymentReceiptSchema"], "deploymentReceiptSha256": deployment["deploymentReceiptSha256"], "deploymentReceiptVerified": deployment["deploymentReceiptVerified"], "runtimeHealthy": deployment["runtimeHealthy"], "publishedHostPorts": deployment["publishedHostPorts"], "globalDeployLockHeld": quiescence.get("globalDeployLockHeld"), "projectLockHeld": quiescence.get("projectLockHeld"), "legacyVolume": deployment["legacyVolume"], "legacyVolumeRetained": deployment["legacyVolumeRetained"], "legacyEnvironmentRetained": deployment["legacyEnvironmentRetained"], "managedEnvironmentFilePrivate": deployment["managedEnvironmentFilePrivate"], "scanSourceMatches": deployment["scanSourceMatches"], "scanMountReadOnly": deployment["scanMountReadOnly"], "scanPermissionPolicyPersistent": deployment["scanPermissionPolicyPersistent"], "scanAccessForRuntimeIdentityReady": deployment[ "scanAccessForRuntimeIdentityReady" ], "controllerIntentDurable": quiescence.get("controllerIntentDurable"), "controllerIntentBindingSha256": quiescence.get( "controllerIntentBindingSha256" ), } # Run the exact pure state machine in memory before committing any # checkpoints. This also verifies the result against the fresh preflight. candidate = protocol.advance_intent(intent, "target_gates", gates) candidate = protocol.advance_intent(candidate, "quiescence", quiescence) candidate = protocol.advance_intent(candidate, "snapshot", snapshot) candidate = protocol.advance_intent(candidate, "controller_registration", registration) return ( { "target_gates": gates, "quiescence": quiescence, "snapshot": snapshot, "controller_registration": candidate["evidence"]["controllerRegistration"], }, checked_registry["receiptSha256"], checked_registry["projectsSha256"], ) def _validate_status_binding( value: object, intent: dict[str, Any], registration: dict[str, Any], *, controller_intent_sha256: str, registry_receipt_sha256: str, projects_sha256: str, ) -> dict[str, Any]: status = _status_projection(value) hashes = status["hashes"] snapshot = intent["evidence"]["snapshot"] gates = intent["evidence"]["targetGates"] _need( status.get("status") == "in_progress" and status.get("phase") == "snapshot_verified" and hashes.get("intentSha256") == controller_intent_sha256 and hashes.get("intentBindingSha256") == protocol.intent_binding_sha256(intent) and hashes.get("idempotencySha256") == intent["idempotencySha256"] and hashes.get("preflightSha256") == intent["preflightSha256"] and hashes.get("planSha256") == gates.get("planSha256") and hashes.get("snapshotFileSha256") == snapshot.get("snapshotFileSha256") and hashes.get("snapshotLogicalSha256") == snapshot.get("snapshotLogicalSha256") and hashes.get("snapshotQueueSha256") == snapshot.get("snapshotQueueSha256") and hashes.get("expectedProjectsSha256") == projects_sha256 and hashes.get("targetProjectsSha256") == projects_sha256 and hashes.get("receiptSha256") == registry_receipt_sha256 and registration.get("receiptSha256") is not None, "controller_status_binding_mismatch", ) return status def _failure_evidence(stage: str, status_value: object) -> dict[str, Any]: return {"stage": stage, **_status_failure_evidence(status_value)} def _mark_recovery( session: Session, status: dict[str, Any], *, reason: str = "unknown_external_effect", stage: str, controller_status: object, ) -> dict[str, Any]: if status.get("status") == "recovery_required" or status.get("status") == "completed": return _safe_status(status) try: updated = journal.mark_repopulse_adoption_recovery( session, reason, _failure_evidence(stage, controller_status), expected_revision=status.get("revision"), ) except Exception: # noqa: BLE001 - persistence failures must become a safe recovery result. try: updated = journal.get_repopulse_adoption_status(session) except Exception: # noqa: BLE001 - never return database exception text. return {"status": "recovery_required", "phase": status.get("phase"), "reason": reason} return _safe_status(updated) class RepoPulseAdoptionRunner: """Operator-only prepare/finalize service for fixed RepoPulse adoption.""" def __init__( self, *, session: Session, preflight: RepoPulseAdoptionPreflight | None = None, broker: DeploymentBrokerClient | None = None, ): self.session = session self._runtime_settings_loaded = False self.preflight = preflight or RepoPulseAdoptionPreflight() self.broker = broker or DeploymentBrokerClient() def _initialize_operator_runtime(self) -> None: if self._runtime_settings_loaded: return # FastAPI routes hydrate connector settings before using Portainer. # The operator CLI has no lifespan, so perform the same bounded load. apply_stored_connector_settings(self.session, {"portainer"}) self._runtime_settings_loaded = True def _fresh_intent(self, idempotency_key: str) -> tuple[dict[str, Any], dict[str, Any]]: _need( isinstance(idempotency_key, str) and _IDEMPOTENCY_KEY.fullmatch(idempotency_key) is not None, "idempotency_key_invalid", ) report = self.preflight.run() candidate = protocol.create_intent(report, idempotency_key) local = journal.reserve_repopulse_adoption( self.session, report, idempotency_key ) row, intent = journal._load(self.session) _need(row is not None and intent is not None, "adoption_intent_not_reserved") _need(intent["intentId"] == candidate["intentId"], "adoption_intent_conflict") return local, intent def _controller_status(self, secret: str) -> dict[str, Any]: return self.broker.repopulse_adoption_status(secret=secret) def prepare(self, idempotency_key: str) -> dict[str, Any]: """Freshly preflight and reserve; never invoke controller cutover.""" if protocol.EXECUTION_ENABLED is not True: return {"status": "disabled", "reason": "repopulse_adoption_execution_disabled"} local: dict[str, Any] | None = None bootstrap: str | None = None reserve_attempted = False try: self._initialize_operator_runtime() local, intent = self._fresh_intent(idempotency_key) if local.get("status") in {"completed", "recovery_required"}: return _safe_status(local) _validate_transport(self.broker) _trust, bootstrap = _trust_and_bootstrap(self.session) handoff = _read_handoff(self.session) if handoff is not None: if not _handoff_matches(handoff, intent): return _mark_recovery( self.session, local, stage="handoff_conflict", controller_status={"status": "unavailable"}, ) remote = self._controller_status(bootstrap) try: remote = _validate_reservation_status(remote, intent) except RepoPulseAdoptionRunnerError: return _mark_recovery( self.session, local, stage="prepare_replay", controller_status=remote, ) if handoff.get("state") == "reserve_attempted": remote_intent_sha = remote["hashes"].get("intentSha256") if not _sha(remote_intent_sha): return _mark_recovery( self.session, local, stage="prepare_replay", controller_status=remote, ) _persist_handoff(self.session, intent, remote_intent_sha) return {**_safe_status(local), "status": "prepared"} if local.get("phase") != "reserved": remote = self._controller_status(bootstrap) return _mark_recovery( self.session, local, stage="handoff_missing", controller_status=remote, ) capabilities = self.broker.repopulse_adoption_capabilities( secret=bootstrap ) _validate_capabilities(capabilities) _persist_reserve_attempt(self.session, intent) reserve_attempted = True try: reservation = self.broker.repopulse_adoption_reserve( secret=bootstrap, intent_id=intent["intentId"], idempotency_sha256=intent["idempotencySha256"], preflight_sha256=intent["preflightSha256"], ) except Exception: # noqa: BLE001 - reserve may have committed remotely. return self._uncertain_prepare(local, "controller_reserve", bootstrap) if isinstance(reservation, dict) and reservation.get("status") == "execution_disabled": return {**_safe_status(local), "status": "blocked", "reason": "controller_execution_disabled"} try: controller_intent_sha = _validate_reservation(reservation, intent) enrollment_id, target_credential = get_or_create_repopulse_target_credential( self.session, intent_id=intent["intentId"] ) del target_credential _need( enrollment_id == f"enr-{intent['intentId'][4:]}", "target_credential_binding_mismatch", ) _persist_handoff(self.session, intent, controller_intent_sha) except Exception: # noqa: BLE001 - the durable reserve may precede local failure. # The controller may have durably reserved before this process # failed; reconcile through status and never POST reserve again. return self._uncertain_prepare(local, "controller_reserve", bootstrap) return {**_safe_status(local), "status": "prepared"} except RepoPulseAdoptionRunnerError as error: if reserve_attempted and local is not None: return self._uncertain_prepare(local, "controller_reserve", bootstrap) return {"status": "blocked", "reason": error.code} except (journal.RepoPulseAdoptionJournalError, protocol.AdoptionProtocolError) as error: if reserve_attempted and local is not None: return self._uncertain_prepare(local, "controller_reserve", bootstrap) return {"status": "blocked", "reason": _safe_error_code(error, "adoption_prepare_failed")} except RepoPulseTargetCredentialError as error: if reserve_attempted and local is not None: return self._uncertain_prepare(local, "controller_reserve", bootstrap) return {"status": "blocked", "reason": _safe_error_code(error, "target_credential_unavailable")} except Exception: # noqa: BLE001 - transport and DB exceptions may contain credentials. # HTTP clients and drivers can include credentials in exception # text; this CLI boundary deliberately emits only fixed codes. try: local = local or journal.get_repopulse_adoption_status(self.session) except Exception: # noqa: BLE001 - status read failure must not expose exception text. return {"status": "blocked", "reason": "adoption_prepare_failed"} if reserve_attempted and local.get("status") == "in_progress": return self._uncertain_prepare(local, "controller_reserve", bootstrap) return _safe_status(local, reason="adoption_prepare_failed") def _uncertain_prepare( self, local: dict[str, Any], stage: str, bootstrap: str | None ) -> dict[str, Any]: remote: object = {"status": "unavailable"} if bootstrap is not None: try: remote = self._controller_status(bootstrap) except Exception: # noqa: BLE001 - record unavailable without logging transport details. remote = {"status": "unavailable"} return _mark_recovery( self.session, local, stage=stage, controller_status=remote, ) def prepare_controller_cli_handoff(self, idempotency_key: str) -> dict[str, Any]: """Return the exact in-memory stdin contract for the fixed root CLI. The caller must pipe this object directly to the separately operated controller CLI. It must not print or persist the target credential. """ prepared = self.prepare(idempotency_key) _need(prepared.get("status") == "prepared", "controller_handoff_not_ready") local = journal.get_repopulse_adoption_status(self.session) _need( local.get("status") == "in_progress" and local.get("phase") == "reserved", "controller_handoff_phase_invalid", ) row, intent = journal._load(self.session) _need(row is not None and intent is not None, "adoption_intent_not_reserved") marker = _read_handoff(self.session) if marker is not None and marker.get("state") == "controller_cli_handoff_issued": _validate_transport(self.broker) _trust, bootstrap = _trust_and_bootstrap(self.session) remote = self._controller_status(bootstrap) _mark_recovery( self.session, local, stage="controller_cli_handoff_replay", controller_status=remote, ) raise RepoPulseAdoptionRunnerError( "controller_cli_handoff_replay_requires_recovery" ) _need( marker is not None and marker.get("state") == "handoff_prepared" and _handoff_matches(marker, intent), "controller_handoff_marker_invalid", ) enrollment_id, target_credential = get_or_create_repopulse_target_credential( self.session, intent_id=intent["intentId"] ) _need( enrollment_id == f"enr-{intent['intentId'][4:]}" and isinstance(target_credential, bytes) and 32 <= len(target_credential) <= 256, "target_credential_binding_mismatch", ) try: credential = target_credential.decode("ascii") except UnicodeDecodeError: raise RepoPulseAdoptionRunnerError("target_credential_encoding_invalid") from None _need(credential.isprintable(), "target_credential_encoding_invalid") projection = intent["evidence"]["preflight"] _need(isinstance(projection, dict), "controller_handoff_preflight_invalid") _persist_cli_handoff_issued( self.session, intent, marker["controllerIntentSha256"] ) return { "intentId": intent["intentId"], "idempotencySha256": intent["idempotencySha256"], "preflightSha256": intent["preflightSha256"], "preflightProjection": dict(projection), "targetCredential": credential, } def finalize(self, controller_result: object) -> dict[str, Any]: """Validate root-controller evidence, checkpoint it, and commit ownership. The caller must obtain ``controller_result`` from the trusted fixed controller handoff. This method validates every field and confirms the matching controller journal over CA-verified HTTPS/HMAC before AppOps writes ownership. """ if protocol.EXECUTION_ENABLED is not True: return {"status": "disabled", "reason": "repopulse_adoption_execution_disabled"} try: local = journal.get_repopulse_adoption_status(self.session) if local.get("status") == "completed": return _safe_status(local) _need(local.get("status") == "in_progress", "adoption_not_ready_to_finalize") _need(local.get("phase") == "reserved", "adoption_partial_finalize_requires_recovery") row, intent = journal._load(self.session) _need(row is not None and intent is not None, "adoption_intent_not_reserved") handoff = _read_handoff(self.session) _need( handoff is not None and handoff.get("state") == "controller_cli_handoff_issued" and _handoff_matches(handoff, intent), "controller_handoff_missing", ) _validate_transport(self.broker) _trust, bootstrap = _trust_and_bootstrap(self.session) mapped, registry_receipt_sha256, projects_sha256 = _map_controller_result( controller_result, intent ) candidate = intent for step in ("target_gates", "quiescence", "snapshot", "controller_registration"): candidate = protocol.advance_intent(candidate, step, mapped[step]) registration = candidate["evidence"]["controllerRegistration"] remote = self._controller_status(bootstrap) _validate_status_binding( remote, candidate, registration, controller_intent_sha256=handoff["controllerIntentSha256"], registry_receipt_sha256=registry_receipt_sha256, projects_sha256=projects_sha256, ) current_status = local for step in ("target_gates", "quiescence", "snapshot", "controller_registration"): current_status = journal.advance_repopulse_adoption( self.session, step, mapped[step], expected_revision=current_status["revision"], ) current_status = ownership.commit_repopulse_adoption_ownership( self.session, registration, expected_revision=current_status["revision"], ) current_status = ownership.complete_repopulse_adoption( self.session, registration, expected_revision=current_status["revision"], ) return _safe_status(current_status) except Exception as error: # noqa: BLE001 - recover any uncertain side effect without echoing it. try: local = journal.get_repopulse_adoption_status(self.session) except Exception: # noqa: BLE001 - never expose database exception text. return {"status": "recovery_required", "reason": "unknown_external_effect"} if local.get("status") == "completed": return _safe_status(local) try: _trust, bootstrap = _trust_and_bootstrap(self.session) _validate_transport(self.broker) remote = self._controller_status(bootstrap) except Exception: # noqa: BLE001 - query failure is represented as unavailable. remote = {"status": "unavailable"} code = _safe_error_code(error, "controller_result_unverified") reason = "appops_commit_uncertain" if code == "appops_commit_uncertain" else "unknown_external_effect" return _mark_recovery( self.session, local, reason=reason, stage="finalize", controller_status=remote, ) def _handoff_matches(handoff: dict[str, Any], intent: dict[str, Any]) -> bool: return bool( handoff.get("intentId") == intent.get("intentId") and handoff.get("idempotencySha256") == intent.get("idempotencySha256") and handoff.get("preflightSha256") == intent.get("preflightSha256") and handoff.get("intentBindingSha256") == protocol.intent_binding_sha256(intent) ) def _validate_reservation_status(value: object, intent: dict[str, Any]) -> dict[str, Any]: status = _status_projection(value) hashes = status["hashes"] _need( status["status"] == "in_progress" and status["phase"] in _RECOVERY_STATUS_PHASES and hashes.get("intentBindingSha256") == protocol.intent_binding_sha256(intent) and hashes.get("idempotencySha256") == intent["idempotencySha256"] and hashes.get("preflightSha256") == intent["preflightSha256"], "controller_status_binding_mismatch", ) return status def main(argv: list[str] | None = None) -> int: """Run fixed operator phases; finalize accepts only bounded JSON stdin.""" parser = argparse.ArgumentParser(description="Operate the fixed one-time RepoPulse adoption") commands = parser.add_subparsers(dest="command", required=True) prepare_parser = commands.add_parser("prepare", help="fresh preflight and reserve") prepare_parser.add_argument("--idempotency-key", required=True) commands.add_parser("finalize", help="verify controller result from stdin") args = parser.parse_args(argv) try: with Session(engine) as session: runner = RepoPulseAdoptionRunner(session=session) if protocol.EXECUTION_ENABLED is not True: result = {"status": "disabled", "reason": "repopulse_adoption_execution_disabled"} elif args.command == "prepare": result = runner.prepare(args.idempotency_key) else: result = runner.finalize(_read_controller_result_stdin()) except Exception: # noqa: BLE001 - CLI boundary emits only a fixed sanitized reason. result = {"status": "blocked", "reason": "adoption_operator_command_failed"} print(json.dumps(result, sort_keys=True, separators=(",", ":"))) return 0 if result.get("status") in {"prepared", "completed"} else 2 if __name__ == "__main__": sys.exit(main())