Files
Unraid-AppOps-Gateway-Public/backend/app/services/repopulse_adoption_runner.py
T

1278 lines
53 KiB
Python

"""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())