630 lines
23 KiB
Python
630 lines
23 KiB
Python
"""Atomic AppOps ownership commit for the one RepoPulse adoption.
|
|
|
|
This is a database-only protocol step. The caller must authenticate the live
|
|
controller receipt before supplying the fixed registration evidence. No route,
|
|
controller client, secret writer, or generic enrollment orchestration is used.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from typing import Any
|
|
|
|
from sqlalchemy import or_, update
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlmodel import Session, select
|
|
|
|
from ..core.time import utc_now
|
|
from ..db.models import AppSetting, Enrollment, Policy, RepoPulseAdoptionJournal
|
|
from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
|
|
from . import repopulse_adoption_journal as journal
|
|
from . import repopulse_legacy_adoption_protocol as protocol
|
|
from .enrollment_profiles import PROFILE_VERSION
|
|
from .secret_store import has_secret
|
|
|
|
_SLOT = "repopulse"
|
|
_TRUST_SETTING_KEY = "deployment_enrollment:trust"
|
|
_CONTROLLER_IDENTITY = "unraid-controller"
|
|
_TARGET_SECRET_REF = "ENROLL_REPOPULSE_TARGET_HMAC"
|
|
_CREATED_BY = "repopulse-legacy-adoption/v1"
|
|
_APPLY_POLICY_ID = "enrollment-repopulse-deployment-apply"
|
|
_ROLLBACK_POLICY_ID = "enrollment-repopulse-deployment-rollback"
|
|
_POLICY_REASON = "Enrollment-generated exact-target deployment policy."
|
|
|
|
|
|
class RepoPulseOwnershipError(ValueError):
|
|
"""Sanitized fixed ownership precondition or persistence error."""
|
|
|
|
def __init__(self, code: str):
|
|
super().__init__(code)
|
|
self.code = code
|
|
|
|
|
|
def _need(condition: bool, code: str) -> None:
|
|
if not condition:
|
|
raise RepoPulseOwnershipError(code)
|
|
|
|
|
|
def _canonical_plan(plan: EnrollmentPlanV1) -> tuple[dict[str, Any], str]:
|
|
payload = plan.model_dump(mode="json")
|
|
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
return payload, hashlib.sha256(encoded).hexdigest()
|
|
|
|
|
|
def _fixed_plan(
|
|
session: Session, intent: dict[str, Any]
|
|
) -> tuple[EnrollmentPlanV1, dict[str, Any], str]:
|
|
preflight = intent["evidence"].get("preflight")
|
|
gates = intent["evidence"].get("targetGates")
|
|
registration = intent["evidence"].get("controllerRegistration")
|
|
_need(
|
|
isinstance(preflight, dict) and isinstance(gates, dict),
|
|
"adoption_prerequisite_missing",
|
|
)
|
|
_need(isinstance(registration, dict), "controller_registration_missing")
|
|
|
|
trust_row = session.get(AppSetting, _TRUST_SETTING_KEY)
|
|
_need(
|
|
trust_row is not None and trust_row.is_secret is False,
|
|
"enrollment_trust_unavailable",
|
|
)
|
|
try:
|
|
trust = EnrollmentTrustConfigV1.model_validate(trust_row.value_json)
|
|
except (TypeError, ValueError):
|
|
raise RepoPulseOwnershipError("enrollment_trust_invalid") from None
|
|
_need(
|
|
trust.controller_identity == _CONTROLLER_IDENTITY,
|
|
"controller_identity_mismatch",
|
|
)
|
|
_need(
|
|
"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",
|
|
)
|
|
|
|
revision = preflight.get("main_revision")
|
|
_need(
|
|
isinstance(revision, str) and len(revision) == 40, "preflight_revision_invalid"
|
|
)
|
|
try:
|
|
plan = EnrollmentPlanV1.model_validate(
|
|
{
|
|
"application_id": protocol.APPLICATION_ID,
|
|
"display_name": "RepoPulse",
|
|
"environment": "production",
|
|
"repository": "Jens/RepoPulse",
|
|
"revision": revision,
|
|
"profile": "repopulse-authenticated",
|
|
"target": {
|
|
"target_identity": _CONTROLLER_IDENTITY,
|
|
"container_name": "repopulse",
|
|
},
|
|
"storage": {"runtime_uid": 10001, "runtime_gid": 10001},
|
|
"network": {
|
|
"docker_network": "itworx-mcp-hub-npm",
|
|
"internal_port": 8000,
|
|
},
|
|
"artifact": {
|
|
"release_version": "1.5.0",
|
|
"image_repository": "repopulse-managed",
|
|
"image_tag": "production",
|
|
"health_path": "/health",
|
|
},
|
|
"exposure": {
|
|
"hostname": "repopulse.example.com",
|
|
"upstream_service": "repopulse",
|
|
},
|
|
"secrets": {"additional_secret_refs": ["REPOPULSE_GITEA_TOKEN"]},
|
|
}
|
|
)
|
|
except (TypeError, ValueError):
|
|
raise RepoPulseOwnershipError("fixed_repopulse_plan_invalid") from None
|
|
|
|
payload, plan_sha256 = _canonical_plan(plan)
|
|
_need(
|
|
gates.get("profileId") == "repopulse-authenticated"
|
|
and gates.get("profileVersion") == PROFILE_VERSION
|
|
and gates.get("planSha256") == plan_sha256
|
|
and registration.get("planSha256") == plan_sha256,
|
|
"fixed_repopulse_plan_digest_mismatch",
|
|
)
|
|
expected_enrollment_id = f"enr-{intent['intentId'][4:]}"
|
|
_need(
|
|
registration.get("enrollmentId") == expected_enrollment_id,
|
|
"fixed_enrollment_id_mismatch",
|
|
)
|
|
return plan, payload, plan_sha256
|
|
|
|
|
|
def _expected_provenance(
|
|
intent: dict[str, Any], registration: dict[str, Any], plan_sha256: str
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"createdBy": _CREATED_BY,
|
|
"adoptionIntentId": intent["intentId"],
|
|
"profileVersion": PROFILE_VERSION,
|
|
"policyTemplateVersion": "exact-target/v1",
|
|
"sourceRevision": intent["evidence"]["preflight"]["main_revision"],
|
|
"planSha256": plan_sha256,
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"deploymentReceiptSha256": registration["deploymentReceiptSha256"],
|
|
}
|
|
|
|
|
|
def _expected_resources(registration: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"appdataPath": "/srv/example/appdata/repopulse",
|
|
"dataPath": "/srv/example/appdata/repopulse/data",
|
|
"network": "itworx-mcp-hub-npm",
|
|
"hostname": "repopulse.example.com",
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"deploymentReceiptSha256": registration["deploymentReceiptSha256"],
|
|
}
|
|
|
|
|
|
def _expected_policy_rows(
|
|
intent: dict[str, Any], registration: dict[str, Any], plan_sha256: str
|
|
) -> tuple[Policy, Policy]:
|
|
provenance = _expected_provenance(intent, registration, plan_sha256)
|
|
return (
|
|
Policy(
|
|
id=_APPLY_POLICY_ID,
|
|
action="deployment.apply",
|
|
target=protocol.APPLICATION_ID,
|
|
voice="blocked",
|
|
manual="allowed",
|
|
risk="high",
|
|
reason=_POLICY_REASON,
|
|
provenance_json=provenance,
|
|
),
|
|
Policy(
|
|
id=_ROLLBACK_POLICY_ID,
|
|
action="deployment.rollback",
|
|
target=protocol.APPLICATION_ID,
|
|
voice="blocked",
|
|
manual="allowed",
|
|
risk="high",
|
|
reason=_POLICY_REASON,
|
|
provenance_json=provenance,
|
|
),
|
|
)
|
|
|
|
|
|
def _policy_matches(actual: Policy | None, expected: Policy) -> bool:
|
|
return bool(
|
|
actual is not None
|
|
and actual.id == expected.id
|
|
and actual.action == expected.action
|
|
and actual.target == expected.target
|
|
and actual.voice == expected.voice
|
|
and actual.manual == expected.manual
|
|
and actual.risk == expected.risk
|
|
and actual.reason == expected.reason
|
|
and actual.provenance_json == expected.provenance_json
|
|
)
|
|
|
|
|
|
def _expected_enrollment(
|
|
intent: dict[str, Any],
|
|
registration: dict[str, Any],
|
|
plan: EnrollmentPlanV1,
|
|
plan_json: dict[str, Any],
|
|
plan_sha256: str,
|
|
) -> Enrollment:
|
|
return Enrollment(
|
|
id=registration["enrollmentId"],
|
|
application_id=protocol.APPLICATION_ID,
|
|
repository=protocol.REPOSITORY,
|
|
revision=plan.revision,
|
|
profile="repopulse-authenticated",
|
|
controller_identity=_CONTROLLER_IDENTITY,
|
|
plan_sha256=plan_sha256,
|
|
status="ready",
|
|
target_secret_ref=_TARGET_SECRET_REF,
|
|
oidc_secret_ref=None,
|
|
plan_json=plan_json,
|
|
resources_json=_expected_resources(registration),
|
|
provenance_json=_expected_provenance(intent, registration, plan_sha256),
|
|
)
|
|
|
|
|
|
def _enrollment_matches(actual: Enrollment | None, expected: Enrollment) -> bool:
|
|
return bool(
|
|
actual is not None
|
|
and actual.id == expected.id
|
|
and actual.application_id == expected.application_id
|
|
and actual.repository == expected.repository
|
|
and actual.revision == expected.revision
|
|
and actual.profile == expected.profile
|
|
and actual.controller_identity == expected.controller_identity
|
|
and actual.plan_sha256 == expected.plan_sha256
|
|
and actual.status == expected.status
|
|
and actual.target_secret_ref == expected.target_secret_ref
|
|
and actual.oidc_secret_ref is None
|
|
and actual.plan_json == expected.plan_json
|
|
and actual.resources_json == expected.resources_json
|
|
and actual.provenance_json == expected.provenance_json
|
|
and actual.last_verified_at is not None
|
|
and actual.completed_at is not None
|
|
)
|
|
|
|
|
|
def _verify_owned_rows(
|
|
session: Session,
|
|
intent: dict[str, Any],
|
|
registration: dict[str, Any],
|
|
plan: EnrollmentPlanV1,
|
|
plan_json: dict[str, Any],
|
|
plan_sha256: str,
|
|
) -> tuple[Enrollment, tuple[Policy, Policy]]:
|
|
expected_enrollment = _expected_enrollment(
|
|
intent, registration, plan, plan_json, plan_sha256
|
|
)
|
|
matching_enrollments = session.exec(
|
|
select(Enrollment).where(
|
|
or_(
|
|
Enrollment.application_id == protocol.APPLICATION_ID,
|
|
Enrollment.repository == protocol.REPOSITORY,
|
|
)
|
|
)
|
|
).all()
|
|
_need(
|
|
len(matching_enrollments) == 1
|
|
and _enrollment_matches(matching_enrollments[0], expected_enrollment),
|
|
"ownership_enrollment_readback_mismatch",
|
|
)
|
|
expected_policies = _expected_policy_rows(intent, registration, plan_sha256)
|
|
_need(
|
|
all(
|
|
_policy_matches(session.get(Policy, expected.id), expected)
|
|
for expected in expected_policies
|
|
),
|
|
"ownership_policy_readback_mismatch",
|
|
)
|
|
_need(
|
|
has_secret(session, _TARGET_SECRET_REF), "target_secret_reference_unavailable"
|
|
)
|
|
return matching_enrollments[0], expected_policies
|
|
|
|
|
|
def _verify_live_registration(
|
|
intent: dict[str, Any], live_registration_evidence: object
|
|
) -> dict[str, Any]:
|
|
try:
|
|
if intent.get("status") == "completed":
|
|
checked = protocol.validate_controller_registration_evidence(
|
|
intent, live_registration_evidence
|
|
)
|
|
replayed_registration = checked
|
|
else:
|
|
replayed = protocol.advance_intent(
|
|
intent, "controller_registration", live_registration_evidence
|
|
)
|
|
replayed_registration = replayed["evidence"].get("controllerRegistration")
|
|
except protocol.AdoptionProtocolError as error:
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
_need(
|
|
replayed_registration == intent["evidence"].get("controllerRegistration"),
|
|
"idempotency_conflict",
|
|
)
|
|
return replayed_registration
|
|
|
|
|
|
def _ownership_evidence(
|
|
registration: dict[str, Any], plan_sha256: str
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"repository": protocol.REPOSITORY,
|
|
"applicationId": protocol.APPLICATION_ID,
|
|
"enrollmentId": registration["enrollmentId"],
|
|
"profileId": registration["profileId"],
|
|
"planSha256": plan_sha256,
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"enrollmentPersisted": True,
|
|
"exactApplyPolicyPersisted": True,
|
|
"exactRollbackPolicyPersisted": True,
|
|
"secretReferenceReady": True,
|
|
}
|
|
|
|
|
|
def _advance_or_verify_ownership(
|
|
intent: dict[str, Any], evidence: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
try:
|
|
if intent.get("status") == "completed":
|
|
checked = protocol.validate_appops_ownership_evidence(intent, evidence)
|
|
_need(
|
|
checked == intent["evidence"].get("appopsOwnership"),
|
|
"idempotency_conflict",
|
|
)
|
|
return intent
|
|
return protocol.advance_intent(intent, "appops_ownership", evidence)
|
|
except protocol.AdoptionProtocolError as error:
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
|
|
|
|
def _write_journal_update(
|
|
session: Session,
|
|
row: RepoPulseAdoptionJournal,
|
|
current: dict[str, Any],
|
|
updated: dict[str, Any],
|
|
expected_revision: int,
|
|
) -> int:
|
|
result = session.execute(
|
|
update(RepoPulseAdoptionJournal)
|
|
.where(
|
|
RepoPulseAdoptionJournal.slot == _SLOT,
|
|
RepoPulseAdoptionJournal.revision == expected_revision,
|
|
RepoPulseAdoptionJournal.intent_id == current["intentId"],
|
|
)
|
|
.values(
|
|
idempotency_sha256=updated["idempotencySha256"],
|
|
preflight_sha256=updated["preflightSha256"],
|
|
phase=updated["phase"],
|
|
status=updated["status"],
|
|
intent_json=updated,
|
|
revision=expected_revision + 1,
|
|
updated_at=utc_now(),
|
|
)
|
|
.execution_options(synchronize_session=False)
|
|
)
|
|
_need(result.rowcount == 1, "journal_revision_conflict")
|
|
return expected_revision + 1
|
|
|
|
|
|
def _readback_owned(
|
|
session: Session, registration: dict[str, Any]
|
|
) -> dict[str, Any] | None:
|
|
session.rollback()
|
|
session.expire_all()
|
|
try:
|
|
row, intent = journal._load(session)
|
|
if row is None or intent is None:
|
|
return None
|
|
live = _verify_live_registration(intent, registration)
|
|
plan, plan_json, plan_sha256 = _fixed_plan(session, intent)
|
|
_verify_owned_rows(session, intent, live, plan, plan_json, plan_sha256)
|
|
_advance_or_verify_ownership(intent, _ownership_evidence(live, plan_sha256))
|
|
if intent["phase"] not in {"appops_owned", "completed"}:
|
|
return None
|
|
if intent["phase"] == "completed":
|
|
_assert_completion_binding(intent, live, registration)
|
|
return journal.get_repopulse_adoption_status(session)
|
|
except (
|
|
RepoPulseOwnershipError,
|
|
journal.RepoPulseAdoptionJournalError,
|
|
protocol.AdoptionProtocolError,
|
|
SQLAlchemyError,
|
|
):
|
|
session.rollback()
|
|
return None
|
|
|
|
|
|
def _record_uncertain_commit(
|
|
session: Session, expected_revision: int, receipt_sha256: str
|
|
) -> None:
|
|
session.rollback()
|
|
session.expire_all()
|
|
try:
|
|
row, current = journal._load(session)
|
|
if (
|
|
row is None
|
|
or current is None
|
|
or row.revision != expected_revision
|
|
or current.get("phase") != "target_registered"
|
|
or current.get("status") != "in_progress"
|
|
):
|
|
return
|
|
journal.mark_repopulse_adoption_recovery(
|
|
session,
|
|
"appops_commit_uncertain",
|
|
{"controllerReceiptSha256": receipt_sha256},
|
|
expected_revision=expected_revision,
|
|
)
|
|
except (
|
|
RepoPulseOwnershipError,
|
|
journal.RepoPulseAdoptionJournalError,
|
|
protocol.AdoptionProtocolError,
|
|
SQLAlchemyError,
|
|
):
|
|
session.rollback()
|
|
|
|
|
|
def _assert_completion_binding(
|
|
intent: dict[str, Any],
|
|
registration: dict[str, Any],
|
|
live_registration: dict[str, Any],
|
|
) -> None:
|
|
stored = intent.get("evidence", {}).get("completion")
|
|
_need(isinstance(stored, dict), "completion_evidence_missing")
|
|
expected = {
|
|
"intentId": intent["intentId"],
|
|
"repository": protocol.REPOSITORY,
|
|
"applicationId": protocol.APPLICATION_ID,
|
|
"enrollmentId": registration["enrollmentId"],
|
|
"controllerReceiptSha256": registration["receiptSha256"],
|
|
"enrollmentStillPersisted": True,
|
|
"exactApplyPolicyStillPersisted": True,
|
|
"exactRollbackPolicyStillPersisted": True,
|
|
"controllerReceiptStillMatches": (
|
|
live_registration == registration
|
|
and live_registration["receiptSha256"] == registration["receiptSha256"]
|
|
),
|
|
"legacyVolumeRetained": live_registration["legacyVolumeRetained"],
|
|
}
|
|
_need(stored == expected, "completion_binding_mismatch")
|
|
|
|
|
|
def commit_repopulse_adoption_ownership(
|
|
session: Session,
|
|
authenticated_controller_registration: object,
|
|
*,
|
|
expected_revision: int,
|
|
) -> dict[str, Any]:
|
|
"""Atomically install fixed enrollment/policies and advance to `appops_owned`.
|
|
|
|
The caller is responsible for authenticating the live controller response.
|
|
The protocol validator then binds every field to the persisted fixed intent.
|
|
Exact replay reads back the DB rows and validates the fresh receipt again.
|
|
"""
|
|
try:
|
|
row, current = journal._load(session)
|
|
_need(row is not None and current is not None, "adoption_intent_not_reserved")
|
|
live = _verify_live_registration(current, authenticated_controller_registration)
|
|
plan, plan_json, plan_sha256 = _fixed_plan(session, current)
|
|
_need(
|
|
has_secret(session, _TARGET_SECRET_REF),
|
|
"target_secret_reference_unavailable",
|
|
)
|
|
|
|
if current["phase"] in {"appops_owned", "completed"}:
|
|
_verify_owned_rows(session, current, live, plan, plan_json, plan_sha256)
|
|
_advance_or_verify_ownership(
|
|
current, _ownership_evidence(live, plan_sha256)
|
|
)
|
|
if current["phase"] == "completed":
|
|
_assert_completion_binding(current, live, live)
|
|
return journal.get_repopulse_adoption_status(session)
|
|
|
|
_need(
|
|
current["phase"] == "target_registered"
|
|
and current["status"] == "in_progress",
|
|
"adoption_not_ready_for_ownership",
|
|
)
|
|
_need(
|
|
isinstance(expected_revision, int)
|
|
and not isinstance(expected_revision, bool)
|
|
and expected_revision == row.revision,
|
|
"journal_revision_conflict",
|
|
)
|
|
|
|
conflicting_enrollments = session.exec(
|
|
select(Enrollment).where(
|
|
or_(
|
|
Enrollment.application_id == protocol.APPLICATION_ID,
|
|
Enrollment.repository == protocol.REPOSITORY,
|
|
)
|
|
)
|
|
).all()
|
|
_need(not conflicting_enrollments, "ownership_resource_conflict")
|
|
expected_enrollment = _expected_enrollment(
|
|
current, live, plan, plan_json, plan_sha256
|
|
)
|
|
expected_policies = _expected_policy_rows(current, live, plan_sha256)
|
|
_need(
|
|
all(session.get(Policy, item.id) is None for item in expected_policies),
|
|
"ownership_resource_conflict",
|
|
)
|
|
|
|
now = utc_now()
|
|
expected_enrollment.created_at = now
|
|
expected_enrollment.updated_at = now
|
|
expected_enrollment.last_verified_at = now
|
|
expected_enrollment.completed_at = now
|
|
for policy in expected_policies:
|
|
session.add(policy)
|
|
session.add(expected_enrollment)
|
|
session.flush()
|
|
|
|
updated = _advance_or_verify_ownership(
|
|
current, _ownership_evidence(live, plan_sha256)
|
|
)
|
|
next_revision = _write_journal_update(
|
|
session, row, current, updated, expected_revision
|
|
)
|
|
try:
|
|
session.commit()
|
|
except (SQLAlchemyError, RuntimeError, OSError):
|
|
replay = _readback_owned(session, live)
|
|
if replay is not None:
|
|
return replay
|
|
_record_uncertain_commit(session, expected_revision, live["receiptSha256"])
|
|
raise RepoPulseOwnershipError("appops_commit_uncertain") from None
|
|
return journal._status(updated, next_revision)
|
|
except RepoPulseOwnershipError:
|
|
session.rollback()
|
|
raise
|
|
except journal.RepoPulseAdoptionJournalError as error:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
except protocol.AdoptionProtocolError as error:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
except SQLAlchemyError:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError("ownership_storage_unavailable") from None
|
|
|
|
|
|
def complete_repopulse_adoption(
|
|
session: Session,
|
|
authenticated_controller_registration: object,
|
|
*,
|
|
expected_revision: int,
|
|
) -> dict[str, Any]:
|
|
"""Persist AppOps completion after ownership, with fresh receipt readback.
|
|
|
|
Controller authentication and its completed adoption/deployment receipt
|
|
verification remain caller responsibilities. This step independently
|
|
rechecks the receipt binding and exact owned rows on every replay.
|
|
"""
|
|
try:
|
|
row, current = journal._load(session)
|
|
_need(row is not None and current is not None, "adoption_intent_not_reserved")
|
|
live = _verify_live_registration(current, authenticated_controller_registration)
|
|
plan, plan_json, plan_sha256 = _fixed_plan(session, current)
|
|
_verify_owned_rows(session, current, live, plan, plan_json, plan_sha256)
|
|
_advance_or_verify_ownership(current, _ownership_evidence(live, plan_sha256))
|
|
_need(
|
|
(current["phase"] == "appops_owned" and current["status"] == "in_progress")
|
|
or (current["phase"] == "completed" and current["status"] == "completed"),
|
|
"adoption_not_ready_to_complete",
|
|
)
|
|
completion_evidence = {
|
|
"intentId": current["intentId"],
|
|
"repository": protocol.REPOSITORY,
|
|
"applicationId": protocol.APPLICATION_ID,
|
|
"enrollmentId": live["enrollmentId"],
|
|
"controllerReceiptSha256": live["receiptSha256"],
|
|
"enrollmentStillPersisted": True,
|
|
"exactApplyPolicyStillPersisted": True,
|
|
"exactRollbackPolicyStillPersisted": True,
|
|
"controllerReceiptStillMatches": True,
|
|
"legacyVolumeRetained": live["legacyVolumeRetained"],
|
|
}
|
|
if current["phase"] == "completed":
|
|
_assert_completion_binding(current, live, live)
|
|
_need(
|
|
current["evidence"].get("completion") == completion_evidence,
|
|
"completion_binding_mismatch",
|
|
)
|
|
return journal.get_repopulse_adoption_status(session)
|
|
|
|
_need(
|
|
isinstance(expected_revision, int)
|
|
and not isinstance(expected_revision, bool)
|
|
and expected_revision == row.revision,
|
|
"journal_revision_conflict",
|
|
)
|
|
try:
|
|
updated = protocol.complete_intent(current, completion_evidence)
|
|
except protocol.AdoptionProtocolError as error:
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
return journal._persist_updated_intent(
|
|
session, row, current, updated, expected_revision
|
|
)
|
|
except RepoPulseOwnershipError:
|
|
session.rollback()
|
|
raise
|
|
except journal.RepoPulseAdoptionJournalError as error:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
except protocol.AdoptionProtocolError as error:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError(error.code) from None
|
|
except SQLAlchemyError:
|
|
session.rollback()
|
|
raise RepoPulseOwnershipError("ownership_storage_unavailable") from None
|