#!/usr/bin/env python3 """Converge four Quote approval metadata blobs to singular action references. This narrow production reconcile treats four retained XML references as authority only after one explicit sanitization: user names inside ``assignedApprover`` blocks are replaced with the manifest-bound placeholder. The sanitized XML must be semantically identical to one of the manifest's exact before/after states; formatting-only inter-element whitespace is not authoritative. Live identities are never copied from the seed and never emitted as evidence. The database is used in two sessions. The first performs a payload-private HEX read which is parsed and validated in memory. Desired live bytes are then derived by changing only the seven plural wrapper tag pairs in those live bytes. The second session locks every broad name match and CAS-updates the snapshot only when all four original rows are byte-for-byte unchanged. """ from __future__ import annotations import argparse import base64 from dataclasses import dataclass import hashlib import json import os from pathlib import Path import re import shutil import subprocess import sys import tempfile from typing import Any, Callable, Sequence import xml.etree.ElementTree as ET FORMAT = "quote-approval-action-wrapper-reconcile-v2" EVIDENCE_FORMAT = "quote-approval-action-wrapper-reconcile-evidence-v2" SOURCE_RELPATH = "deployments/offline/runtime/reconcile/quote_approval_action_wrapper_reference.json" DEFAULT_MANIFEST = Path(__file__).with_name("quote_approval_action_wrappers.v2.json") TARGET_DEVELOPER_NAMES = ( "NA_OP_Service_250502", "NA_OP_Service_Partner_250502", "NA_DBaaS_250502", "NA_DBaaS_Partner_250502", ) EXPECTED_USER_APPROVER_COUNTS = (6, 8, 6, 8) IDENTITY_REDACTION = { "kind": "assigned-approver-user-name-v1", "placeholder": "admin@example.invalid", } IDENTITY_PLACEHOLDER = IDENTITY_REDACTION["placeholder"].encode("utf-8") EXPECTED_REFERENCES = ( ("Approved", "FieldUpdate"), ("statusapproved", "FieldUpdate"), ("rejected", "FieldUpdate"), ("statusrejected", "FieldUpdate"), ("Inprocess", "FieldUpdate"), ("Inreview", "FieldUpdate"), ("pending", "FieldUpdate"), ) SHA256_RE = re.compile(r"^[0-9a-f]{64}$") GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") RESET_ATTEMPT_RE = re.compile(r"^[0-9a-f]{32}$") SENTINEL = "QUOTE_APPROVAL_ACTION_WRAPPER_RECONCILE_RESULT:" ASSERTION_TABLE = "_quote_approval_action_wrapper_reconcile_assert" SINGULAR_REFERENCE_RE = re.compile( rb"(?P\s*(?P[^<]+)" rb"\s*(?P[^<]+)\s*)" ) PLURAL_REFERENCE_RE = re.compile( rb"(?P\s*(?P[^<]+)" rb"\s*(?P[^<]+)\s*)" ) ASSIGNED_APPROVER_BLOCK_RE = re.compile( rb"(?P)(?P.*?)(?P)", re.DOTALL, ) EXPLICIT_USER_APPROVER_RE = re.compile( rb"(?P\s*)(?P[^<]+)" rb"(?P\s*user.*?)", re.DOTALL, ) class ReconcileError(RuntimeError): """Raised when the reconcile cannot prove its exact mutation contract.""" @dataclass(frozen=True) class TargetAuthority: developer_name: str canonical_before_metadata: bytes canonical_after_metadata: bytes canonical_before_sha256: str canonical_after_sha256: str explicit_user_approver_count: int @dataclass(frozen=True) class ReconcileAuthority: manifest_path: Path manifest_sha256: str source_path: Path source_sha256: str targets: tuple[TargetAuthority, ...] @dataclass(frozen=True) class DeploymentBinding: backup_reset_receipt_sha256: str backup_reset_identity_sha256: str backup_reset_attempt_id: str resolved_downstream_ref: str resolved_upstream_ref: str resolved_official_app_packs_ref: str release_fingerprint_sha256: str @dataclass(frozen=True) class LiveTarget: id_bytes: bytes developer_name: str developer_name_bytes: bytes table_enum_or_id_bytes: bytes owner_pack_bytes: bytes live_before_metadata: bytes live_after_metadata: bytes live_before_sha256: str live_after_sha256: str explicit_user_approver_count: int pre_state: str @dataclass(frozen=True) class ReconcileResult: targets: tuple[LiveTarget, ...] updated_rows: int def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() def _load_json_bytes(path: Path, *, label: str) -> tuple[dict[str, Any], bytes]: try: raw = path.read_bytes() except OSError as exc: raise ReconcileError(f"unable to read {label}: {path}") from exc try: payload = json.loads(raw) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ReconcileError(f"invalid JSON {label}: {path}") from exc if not isinstance(payload, dict): raise ReconcileError(f"{label} must be a JSON object: {path}") return payload, raw def _require_exact_keys(value: dict[str, Any], expected: set[str], *, label: str) -> None: actual = set(value) if actual != expected: raise ReconcileError( f"{label} keys do not match the contract: " f"missing={sorted(expected - actual)} unexpected={sorted(actual - expected)}" ) def load_manifest(path: Path) -> tuple[dict[str, Any], bytes]: payload, raw = _load_json_bytes(path, label="reconcile manifest") _require_exact_keys( payload, { "format", "table_enum_or_id", "owner_pack_state", "source_path", "identity_redaction", "expected_references", "targets", }, label="reconcile manifest", ) if payload["format"] != FORMAT: raise ReconcileError(f"unsupported reconcile manifest format: {payload['format']!r}") if payload["table_enum_or_id"] != "Quote": raise ReconcileError("reconcile manifest may target only TableEnumOrId='Quote'") if payload["owner_pack_state"] != "empty_string": raise ReconcileError("reconcile manifest OwnerPack state must be the reviewed empty string") if payload["source_path"] != SOURCE_RELPATH: raise ReconcileError("reconcile manifest does not name the reviewed Quote wrapper reference") if payload["identity_redaction"] != IDENTITY_REDACTION: raise ReconcileError("reconcile manifest identity redaction is not the reviewed contract") raw_references = payload["expected_references"] if not isinstance(raw_references, list): raise ReconcileError("reconcile manifest expected_references must be a list") references: list[tuple[str, str]] = [] for index, raw_reference in enumerate(raw_references): if not isinstance(raw_reference, dict): raise ReconcileError(f"expected reference {index} must be an object") _require_exact_keys(raw_reference, {"name", "type"}, label=f"expected reference {index}") references.append((str(raw_reference["name"]), str(raw_reference["type"]))) if tuple(references) != EXPECTED_REFERENCES: raise ReconcileError("reconcile manifest reference sequence is not the reviewed seven-item sequence") raw_targets = payload["targets"] if not isinstance(raw_targets, list) or len(raw_targets) != len(TARGET_DEVELOPER_NAMES): raise ReconcileError("reconcile manifest must contain exactly four targets") target_names: list[str] = [] for index, raw_target in enumerate(raw_targets): if not isinstance(raw_target, dict): raise ReconcileError(f"reconcile target {index} must be an object") _require_exact_keys( raw_target, { "developer_name", "canonical_before_metadata_sha256", "canonical_after_metadata_sha256", "explicit_user_approver_count", }, label=f"reconcile target {index}", ) developer_name = str(raw_target["developer_name"]) target_names.append(developer_name) for key in ( "canonical_before_metadata_sha256", "canonical_after_metadata_sha256", ): digest = str(raw_target[key]) if not SHA256_RE.fullmatch(digest): raise ReconcileError(f"reconcile target {developer_name} has invalid {key}") if ( raw_target["canonical_before_metadata_sha256"] == raw_target["canonical_after_metadata_sha256"] ): raise ReconcileError(f"reconcile target {developer_name} has identical before/after hashes") explicit_count = raw_target["explicit_user_approver_count"] if type(explicit_count) is not int or explicit_count != EXPECTED_USER_APPROVER_COUNTS[index]: raise ReconcileError( f"reconcile target {developer_name} has an unreviewed explicit approver count" ) if tuple(target_names) != TARGET_DEVELOPER_NAMES or len(set(target_names)) != len(target_names): raise ReconcileError("reconcile manifest target order/identity is not the exact reviewed set") return payload, raw def _resolve_source(source_root: Path, relpath: str) -> Path: root = source_root.expanduser().resolve(strict=True) candidate = (root / relpath).resolve(strict=True) try: candidate.relative_to(root) except ValueError as exc: raise ReconcileError(f"reconcile source escapes source root: {relpath}") from exc if not candidate.is_file(): raise ReconcileError(f"reconcile source is not a regular file: {relpath}") return candidate def _reference_sequence(pattern: re.Pattern[bytes], payload: bytes) -> tuple[tuple[str, str], ...]: references: list[tuple[str, str]] = [] for match in pattern.finditer(payload): try: references.append( (match.group("name").decode("utf-8"), match.group("type").decode("utf-8")) ) except UnicodeDecodeError as exc: raise ReconcileError("approval metadata reference wrapper is not valid UTF-8") from exc return tuple(references) def _reference_wrapper_state(payload: bytes) -> str: singular_references = _reference_sequence(SINGULAR_REFERENCE_RE, payload) plural_references = _reference_sequence(PLURAL_REFERENCE_RE, payload) if singular_references == EXPECTED_REFERENCES and not plural_references: return "singular" if plural_references == EXPECTED_REFERENCES and not singular_references: return "plural" raise ReconcileError( "approval metadata must contain either all seven exact singular references " "or all seven exact plural references, without a mixed state" ) def derive_before_metadata(after_metadata: bytes) -> bytes: """Change only the seven reviewed singular wrapper tag pairs to plural.""" if _reference_wrapper_state(after_metadata) != "singular": raise ReconcileError("normalized Quote metadata is not in the reviewed singular state") replacements = 0 def replace(match: re.Match[bytes]) -> bytes: nonlocal replacements replacements += 1 return b"" + match.group("body") + b"" before_metadata = SINGULAR_REFERENCE_RE.sub(replace, after_metadata) if replacements != len(EXPECTED_REFERENCES): raise ReconcileError("normalized Quote metadata did not yield exactly seven wrapper replacements") if _reference_wrapper_state(before_metadata) != "plural": raise ReconcileError("derived before metadata is not in the reviewed plural state") return before_metadata def derive_after_metadata(before_metadata: bytes) -> bytes: """Change only the seven reviewed plural wrapper tag pairs to singular.""" if _reference_wrapper_state(before_metadata) != "plural": raise ReconcileError("live Quote metadata is not in the reviewed plural state") replacements = 0 def replace(match: re.Match[bytes]) -> bytes: nonlocal replacements replacements += 1 return b"" + match.group("body") + b"" after_metadata = PLURAL_REFERENCE_RE.sub(replace, before_metadata) if replacements != len(EXPECTED_REFERENCES): raise ReconcileError("live Quote metadata did not yield exactly seven wrapper replacements") if _reference_wrapper_state(after_metadata) != "singular": raise ReconcileError("derived live metadata is not in the reviewed singular state") return after_metadata def _local_xml_name(tag: object) -> str: raw = str(tag) return raw.rsplit("}", 1)[-1] def _explicit_user_approver_count(root: ET.Element) -> int: count = 0 for assigned_approver in root.iter(): if _local_xml_name(assigned_approver.tag) != "assignedApprover": continue for approver in assigned_approver.iter(): if approver is assigned_approver or _local_xml_name(approver.tag) != "approver": continue names = [ child for child in list(approver) if _local_xml_name(child.tag) == "name" ] types = [ child for child in list(approver) if _local_xml_name(child.tag) == "type" ] if any((node.text or "").strip() == "user" for node in types): if len(names) != 1 or len(types) != 1 or (types[0].text or "").strip() != "user": raise ReconcileError( "assignedApprover user entry does not have one exact name/type pair" ) if not (names[0].text or ""): raise ReconcileError("assignedApprover user name is empty") count += 1 return count def canonicalize_metadata( metadata: bytes, *, expected_user_approver_count: int, require_placeholder: bool, ) -> bytes: """Redact only explicit assignedApprover user-name values.""" try: metadata.decode("utf-8") root = ET.fromstring(metadata) except (UnicodeDecodeError, ET.ParseError) as exc: raise ReconcileError("Quote approval metadata is not valid UTF-8 XML") from exc xml_count = _explicit_user_approver_count(root) if xml_count != expected_user_approver_count: raise ReconcileError( "Quote approval metadata explicit user-approver count does not match the manifest" ) replacements = 0 def redact_block(block_match: re.Match[bytes]) -> bytes: nonlocal replacements def redact_approver(approver_match: re.Match[bytes]) -> bytes: nonlocal replacements identity = approver_match.group("name") try: identity.decode("utf-8") except UnicodeDecodeError as exc: raise ReconcileError("assignedApprover user name is not valid UTF-8") from exc if require_placeholder and identity != IDENTITY_PLACEHOLDER: raise ReconcileError( "reviewed Quote wrapper reference contains an unsanitized assignedApprover user name" ) replacements += 1 return ( approver_match.group("prefix") + IDENTITY_PLACEHOLDER + approver_match.group("suffix") ) redacted_body = EXPLICIT_USER_APPROVER_RE.sub( redact_approver, block_match.group("body"), ) return block_match.group("open") + redacted_body + block_match.group("close") canonical = ASSIGNED_APPROVER_BLOCK_RE.sub(redact_block, metadata) if replacements != expected_user_approver_count or replacements != xml_count: raise ReconcileError( "assignedApprover user-name redaction did not cover the exact reviewed entries" ) try: ET.fromstring(canonical) except ET.ParseError as exc: raise ReconcileError("identity-redacted Quote metadata is invalid XML") from exc return canonical def _semantic_xml_tag(tag: object) -> str: if tag is ET.Comment: return "#comment" if tag is ET.ProcessingInstruction: return "#processing-instruction" return str(tag) def _semantic_xml_signature(element: ET.Element) -> tuple[object, ...]: """Return the exact ordered XML structure, excluding indentation whitespace.""" children = list(element) text = element.text or "" if children and not text.strip(): text = "" return ( _semantic_xml_tag(element.tag), tuple(sorted((str(name), value) for name, value in element.attrib.items())), text, tuple( ( _semantic_xml_signature(child), "" if child.tail is None or not child.tail.strip() else child.tail, ) for child in children ), ) def metadata_semantically_equal(left: bytes, right: bytes) -> bool: """Compare XML meaning while preserving byte-level mutation authority.""" try: left_root = ET.fromstring( left, parser=ET.XMLParser( target=ET.TreeBuilder(insert_comments=True, insert_pis=True), ), ) right_root = ET.fromstring( right, parser=ET.XMLParser( target=ET.TreeBuilder(insert_comments=True, insert_pis=True), ), ) except ET.ParseError as exc: raise ReconcileError("canonical Quote approval metadata is not valid XML") from exc return _semantic_xml_signature(left_root) == _semantic_xml_signature(right_root) def load_authority(source_root: Path, manifest_path: Path = DEFAULT_MANIFEST) -> ReconcileAuthority: manifest_path = manifest_path.expanduser().resolve(strict=True) manifest, manifest_raw = load_manifest(manifest_path) source_path = _resolve_source(source_root, str(manifest["source_path"])) seed, source_raw = _load_json_bytes(source_path, label="Quote wrapper reference") definitions = seed.get("processDefinitions") if not isinstance(definitions, list): raise ReconcileError("Quote wrapper reference has no processDefinitions list") by_name: dict[str, list[dict[str, Any]]] = {name: [] for name in TARGET_DEVELOPER_NAMES} for raw_definition in definitions: if not isinstance(raw_definition, dict): continue developer_name = str(raw_definition.get("developerName") or "") if developer_name in by_name: by_name[developer_name].append(raw_definition) manifest_targets = {str(item["developer_name"]): item for item in manifest["targets"]} authorities: list[TargetAuthority] = [] for developer_name in TARGET_DEVELOPER_NAMES: matches = by_name[developer_name] if len(matches) != 1: raise ReconcileError( f"Quote wrapper reference must contain exactly one {developer_name} definition; got {len(matches)}" ) encoded = matches[0].get("metadataBase64") if not isinstance(encoded, str) or not encoded: raise ReconcileError(f"Quote wrapper reference {developer_name} has no metadataBase64") try: source_after_metadata = base64.b64decode(encoded, validate=True) except ValueError as exc: raise ReconcileError(f"normalized Quote metadata is invalid for {developer_name}") from exc manifest_target = manifest_targets[developer_name] explicit_count = manifest_target["explicit_user_approver_count"] canonical_after_metadata = canonicalize_metadata( source_after_metadata, expected_user_approver_count=explicit_count, require_placeholder=True, ) canonical_before_metadata = derive_before_metadata(canonical_after_metadata) try: ET.fromstring(canonical_before_metadata) except ET.ParseError as exc: raise ReconcileError(f"derived before metadata is invalid XML for {developer_name}") from exc canonical_before_sha256 = sha256_bytes(canonical_before_metadata) canonical_after_sha256 = sha256_bytes(canonical_after_metadata) if ( canonical_before_sha256 != manifest_target["canonical_before_metadata_sha256"] ): raise ReconcileError(f"derived before hash drift for {developer_name}") if ( canonical_after_sha256 != manifest_target["canonical_after_metadata_sha256"] ): raise ReconcileError(f"normalized after hash drift for {developer_name}") authorities.append( TargetAuthority( developer_name=developer_name, canonical_before_metadata=canonical_before_metadata, canonical_after_metadata=canonical_after_metadata, canonical_before_sha256=canonical_before_sha256, canonical_after_sha256=canonical_after_sha256, explicit_user_approver_count=explicit_count, ) ) return ReconcileAuthority( manifest_path=manifest_path, manifest_sha256=sha256_bytes(manifest_raw), source_path=source_path, source_sha256=sha256_bytes(source_raw), targets=tuple(authorities), ) def deployment_binding_from_args(args: argparse.Namespace) -> DeploymentBinding: digest_values = { "backup reset receipt": str(args.backup_reset_receipt_sha256), "backup reset identity": str(args.backup_reset_identity_sha256), "release fingerprint": str(args.release_fingerprint_sha256), } for label, digest in digest_values.items(): if not SHA256_RE.fullmatch(digest): raise ReconcileError(f"{label} must be a lowercase SHA-256") attempt_id = str(args.backup_reset_attempt_id) if not RESET_ATTEMPT_RE.fullmatch(attempt_id): raise ReconcileError("backup reset attempt id must be 32 lowercase hexadecimal characters") refs = { "resolved downstream ref": str(args.resolved_downstream_ref), "resolved upstream ref": str(args.resolved_upstream_ref), "resolved official app-packs ref": str(args.resolved_official_app_packs_ref), } for label, ref in refs.items(): if not GIT_SHA_RE.fullmatch(ref): raise ReconcileError(f"{label} must be a 40-character lowercase commit SHA") fingerprint_input = { "backup_reset_attempt_id": attempt_id, "backup_reset_identity_sha256": digest_values["backup reset identity"], "backup_reset_receipt_sha256": digest_values["backup reset receipt"], "resolved_downstream_ref": refs["resolved downstream ref"], "resolved_official_app_packs_ref": refs["resolved official app-packs ref"], "resolved_upstream_ref": refs["resolved upstream ref"], } actual_fingerprint = sha256_bytes( json.dumps( fingerprint_input, ensure_ascii=False, separators=(",", ":"), sort_keys=True, ).encode("utf-8") ) if actual_fingerprint != digest_values["release fingerprint"]: raise ReconcileError("release fingerprint does not match the reset receipt and resolved refs") return DeploymentBinding( backup_reset_receipt_sha256=digest_values["backup reset receipt"], backup_reset_identity_sha256=digest_values["backup reset identity"], backup_reset_attempt_id=attempt_id, resolved_downstream_ref=refs["resolved downstream ref"], resolved_upstream_ref=refs["resolved upstream ref"], resolved_official_app_packs_ref=refs["resolved official app-packs ref"], release_fingerprint_sha256=actual_fingerprint, ) def _sql_string(value: str) -> str: return "'" + value.replace("\\", "\\\\").replace("'", "''") + "'" def _hex_literal(value: bytes) -> str: return "0x" + value.hex() def render_live_read_sql(authority: ReconcileAuthority) -> str: """Render the payload-private first-session read.""" names = ", ".join(_sql_string(target.developer_name) for target in authority.targets) return ( "SET NAMES utf8mb4;\n" "SET @quote_wrapper_tidb_target = TIDB_VERSION();\n" "SELECT HEX(Id), HEX(DeveloperName), HEX(TableEnumOrId), " "HEX(OwnerPack), LOWER(SHA2(Metadata, 256)), HEX(Metadata)\n" " FROM ProcessDefinition\n" f" WHERE DeveloperName IN ({names})\n" " ORDER BY BINARY DeveloperName, BINARY Id;\n" ) def _decode_hex_field( raw: str, *, label: str, allow_empty: bool, allow_null: bool = False, ) -> bytes | None: if raw == "NULL": if allow_null: return None raise ReconcileError(f"live read {label} is NULL") if len(raw) % 2 != 0 or not re.fullmatch(r"[0-9A-F]*", raw): raise ReconcileError(f"live read {label} is not exact uppercase HEX") value = bytes.fromhex(raw) if not allow_empty and not value: raise ReconcileError(f"live read {label} is empty") return value def parse_live_read( authority: ReconcileAuthority, stdout: str, ) -> tuple[LiveTarget, ...]: """Validate exactly four physical rows and derive identity-preserving bytes.""" lines = stdout.splitlines() if len(lines) != len(authority.targets) or any(not line for line in lines): raise ReconcileError("live read did not return exactly four physical rows") rows: dict[str, LiveTarget] = {} seen_ids: set[bytes] = set() authorities = {target.developer_name: target for target in authority.targets} for row_index, line in enumerate(lines): fields = line.split("\t") if len(fields) != 6: raise ReconcileError( f"live read row {row_index} does not contain the exact hash/HEX fields" ) id_bytes = _decode_hex_field( fields[0], label=f"row {row_index} Id", allow_empty=False, ) developer_name_bytes = _decode_hex_field( fields[1], label=f"row {row_index} DeveloperName", allow_empty=False, ) table_enum_or_id_bytes = _decode_hex_field( fields[2], label=f"row {row_index} TableEnumOrId", allow_empty=False, ) owner_pack_bytes = _decode_hex_field( fields[3], label=f"row {row_index} OwnerPack", allow_empty=True, ) database_metadata_sha256 = fields[4] if not SHA256_RE.fullmatch(database_metadata_sha256): raise ReconcileError(f"live read row {row_index} Metadata hash is invalid") metadata = _decode_hex_field( fields[5], label=f"row {row_index} Metadata", allow_empty=False, ) assert isinstance(id_bytes, bytes) assert isinstance(developer_name_bytes, bytes) assert isinstance(table_enum_or_id_bytes, bytes) assert isinstance(owner_pack_bytes, bytes) assert isinstance(metadata, bytes) if sha256_bytes(metadata) != database_metadata_sha256: raise ReconcileError(f"live read row {row_index} Metadata hash does not match its bytes") try: developer_name = developer_name_bytes.decode("utf-8") except UnicodeDecodeError as exc: raise ReconcileError("live read DeveloperName is not valid UTF-8") from exc target_authority = authorities.get(developer_name) if ( target_authority is None or developer_name_bytes != developer_name.encode("utf-8") ): raise ReconcileError("live read contains a non-exact target DeveloperName") if developer_name in rows: raise ReconcileError("live read contains a duplicate target DeveloperName") if id_bytes in seen_ids: raise ReconcileError("live read contains a duplicate physical Id") seen_ids.add(id_bytes) if table_enum_or_id_bytes != b"Quote": raise ReconcileError(f"live read {developer_name} is not an exact Quote row") if owner_pack_bytes != b"": raise ReconcileError(f"live read {developer_name} OwnerPack is not the exact empty value") canonical_metadata = canonicalize_metadata( metadata, expected_user_approver_count=target_authority.explicit_user_approver_count, require_placeholder=False, ) wrapper_state = _reference_wrapper_state(metadata) matches_before = metadata_semantically_equal( canonical_metadata, target_authority.canonical_before_metadata, ) matches_after = metadata_semantically_equal( canonical_metadata, target_authority.canonical_after_metadata, ) if matches_before: if wrapper_state != "plural": raise ReconcileError( f"live read {developer_name} semantic-before XML has the wrong wrapper state" ) pre_state = "known_before" live_after_metadata = derive_after_metadata(metadata) elif matches_after: if wrapper_state != "singular": raise ReconcileError( f"live read {developer_name} semantic-after XML has the wrong wrapper state" ) pre_state = "already_target" live_after_metadata = metadata else: raise ReconcileError( f"live read {developer_name} is neither manifest-bound semantic before nor after" ) desired_canonical = canonicalize_metadata( live_after_metadata, expected_user_approver_count=target_authority.explicit_user_approver_count, require_placeholder=False, ) if not metadata_semantically_equal( desired_canonical, target_authority.canonical_after_metadata, ): raise ReconcileError( f"derived live desired XML does not match semantic after for {developer_name}" ) rows[developer_name] = LiveTarget( id_bytes=id_bytes, developer_name=developer_name, developer_name_bytes=developer_name_bytes, table_enum_or_id_bytes=table_enum_or_id_bytes, owner_pack_bytes=owner_pack_bytes, live_before_metadata=metadata, live_after_metadata=live_after_metadata, live_before_sha256=sha256_bytes(metadata), live_after_sha256=sha256_bytes(live_after_metadata), explicit_user_approver_count=target_authority.explicit_user_approver_count, pre_state=pre_state, ) if set(rows) != set(TARGET_DEVELOPER_NAMES): raise ReconcileError("live read target set does not match the manifest") return tuple(rows[name] for name in TARGET_DEVELOPER_NAMES) def _snapshot_condition(index: int) -> str: return ( f"(BINARY Id = BINARY @quote_wrapper_id_{index} " f"AND BINARY DeveloperName = BINARY @quote_wrapper_name_{index} " f"AND BINARY TableEnumOrId = BINARY @quote_wrapper_object_{index} " f"AND BINARY OwnerPack = BINARY @quote_wrapper_owner_{index} " f"AND BINARY Metadata = BINARY @quote_wrapper_live_before_{index})" ) def _final_condition(index: int) -> str: return ( f"(BINARY Id = BINARY @quote_wrapper_id_{index} " f"AND BINARY DeveloperName = BINARY @quote_wrapper_name_{index} " f"AND BINARY TableEnumOrId = BINARY @quote_wrapper_object_{index} " f"AND BINARY OwnerPack = BINARY @quote_wrapper_owner_{index} " f"AND BINARY Metadata = BINARY @quote_wrapper_live_after_{index})" ) def render_reconcile_sql( authority: ReconcileAuthority, live_targets: Sequence[LiveTarget], *, execution_mode: str, ) -> str: """Render the second-session lock, CAS update, assertion, and outcome.""" if execution_mode not in {"apply", "dry-run"}: raise ReconcileError(f"unsupported execution mode: {execution_mode!r}") if len(live_targets) != len(authority.targets): raise ReconcileError("live target count does not match reconcile authority") if tuple(target.developer_name for target in live_targets) != TARGET_DEVELOPER_NAMES: raise ReconcileError("live target order does not match reconcile authority") names = ", ".join(_sql_string(target.developer_name) for target in authority.targets) variable_lines: list[str] = [] snapshot_conditions: list[str] = [] update_conditions: list[str] = [] update_cases: list[str] = [] final_conditions: list[str] = [] expected_updated_rows = 0 for index, target in enumerate(live_targets): variable_lines.extend( [ f"SET @quote_wrapper_id_{index} = {_hex_literal(target.id_bytes)};", f"SET @quote_wrapper_name_{index} = {_hex_literal(target.developer_name_bytes)};", f"SET @quote_wrapper_object_{index} = {_hex_literal(target.table_enum_or_id_bytes)};", f"SET @quote_wrapper_owner_{index} = '';", f"SET @quote_wrapper_live_before_{index} = {_hex_literal(target.live_before_metadata)};", f"SET @quote_wrapper_live_after_{index} = {_hex_literal(target.live_after_metadata)};", ] ) snapshot_conditions.append(_snapshot_condition(index)) final_conditions.append(_final_condition(index)) if target.pre_state == "known_before": expected_updated_rows += 1 update_conditions.append(_snapshot_condition(index)) update_cases.append( f"WHEN {_snapshot_condition(index)} THEN @quote_wrapper_live_after_{index}" ) elif target.pre_state != "already_target": raise ReconcileError(f"unsupported live pre-state: {target.pre_state!r}") snapshot_sql = " OR\n ".join(snapshot_conditions) update_sql = ( " OR\n ".join(update_conditions) if update_conditions else "0 = 1" ) cases_sql = ( "\n ".join(update_cases) if update_cases else "WHEN 0 = 1 THEN Metadata" ) final_sql = " OR\n ".join(final_conditions) transaction_end = "COMMIT;" if execution_mode == "apply" else "ROLLBACK;" return ( "SET NAMES utf8mb4;\n" "SET @quote_wrapper_tidb_target = TIDB_VERSION();\n" f"DROP TEMPORARY TABLE IF EXISTS `{ASSERTION_TABLE}`;\n" f"CREATE TEMPORARY TABLE `{ASSERTION_TABLE}` " "(`id` TINYINT NOT NULL PRIMARY KEY) ENGINE=MEMORY;\n" f"INSERT INTO `{ASSERTION_TABLE}` (`id`) VALUES (1);\n" + "\n".join(variable_lines) + "\nSTART TRANSACTION;\n" "SELECT HEX(Id)\n" " FROM ProcessDefinition\n" f" WHERE DeveloperName IN ({names})\n" " ORDER BY BINARY DeveloperName, BINARY Id\n" " FOR UPDATE;\n" "SET @quote_wrapper_guard_ok = (\n" " SELECT IF(\n" f" COUNT(*) = {len(live_targets)}\n" " AND SUM(CASE WHEN\n" f" {snapshot_sql}\n" f" THEN 1 ELSE 0 END) = {len(live_targets)},\n" " 1, 0\n" " )\n" " FROM ProcessDefinition\n" f" WHERE DeveloperName IN ({names})\n" ");\n" "UPDATE ProcessDefinition\n" " SET Metadata = CASE\n" f" {cases_sql}\n" " ELSE Metadata\n" " END,\n" " LastModifiedDate = CURRENT_TIMESTAMP\n" " WHERE @quote_wrapper_guard_ok = 1\n" " AND (\n" f" {update_sql}\n" " );\n" "SET @quote_wrapper_updated_rows = ROW_COUNT();\n" "SET @quote_wrapper_final_ok = (\n" " SELECT IF(\n" f" COUNT(*) = {len(live_targets)}\n" " AND SUM(CASE WHEN\n" f" {final_sql}\n" f" THEN 1 ELSE 0 END) = {len(live_targets)},\n" " 1, 0\n" " )\n" " FROM ProcessDefinition\n" f" WHERE DeveloperName IN ({names})\n" ");\n" f"INSERT INTO `{ASSERTION_TABLE}` (`id`)\n" "SELECT IF(\n" " @quote_wrapper_guard_ok = 1\n" " AND @quote_wrapper_final_ok = 1\n" f" AND @quote_wrapper_updated_rows = {expected_updated_rows},\n" " 2, 1\n" ");\n" "SELECT CONCAT(\n" f" '{SENTINEL}',\n" " JSON_OBJECT(\n" " 'guard_ok', @quote_wrapper_guard_ok,\n" " 'final_ok', @quote_wrapper_final_ok,\n" " 'updated_rows', @quote_wrapper_updated_rows,\n" f" 'expected_updated_rows', {expected_updated_rows},\n" f" 'target_count', {len(live_targets)}\n" " )\n" ");\n" f"{transaction_end}\n" f"DROP TEMPORARY TABLE `{ASSERTION_TABLE}`;\n" ) TIDB_TLS_TRUE_TOKENS = frozenset({"1", "true", "yes", "on"}) def _tidb_tls_enabled(raw: str) -> bool: return str(raw).strip().lower() in TIDB_TLS_TRUE_TOKENS def _mysql_client_args(tidb_env: dict[str, str]) -> list[str]: command = shutil.which("mysql") or shutil.which("mariadb") if not command: raise ReconcileError("missing required TiDB SQL client command: mysql or mariadb") required_env = ( "TIDB_HOST", "TIDB_PORT", "TIDB_USER", "TIDB_PASSWORD", "TIDB_DATABASE", "TIDB_TLS", ) missing = [key for key in required_env if not str(tidb_env.get(key, "")).strip()] if missing: raise ReconcileError(f"missing required TiDB environment values: {', '.join(missing)}") try: port = int(str(tidb_env["TIDB_PORT"])) except ValueError as exc: raise ReconcileError("TIDB_PORT must be an integer") from exc if not 1 <= port <= 65535: raise ReconcileError("TIDB_PORT must be between 1 and 65535") if not _tidb_tls_enabled(tidb_env["TIDB_TLS"]): raise ReconcileError( "TiDB approval reconcile requires TIDB_TLS to be one of: 1, true, yes, on" ) probe_env = {"PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")} version_result = subprocess.run( [command, "--no-defaults", "--version"], text=True, capture_output=True, check=False, env=probe_env, ) version_text = f"{version_result.stdout}\n{version_result.stderr}" if version_result.returncode != 0: raise ReconcileError("TiDB SQL client version probe failed") if re.search(r"\bmariadb\b", version_text, re.IGNORECASE): client_family = "mariadb" elif re.search(r"\bmysql\b", version_text, re.IGNORECASE): client_family = "mysql" else: raise ReconcileError("TiDB approval reconcile could not verify SQL client identity") # Option files may set `force`, reconnect, or other session-changing client # behavior. The duplicate-key assertion is our pre-COMMIT abort gate, so no # ambient client configuration is allowed to weaken it. MariaDB has no # login-path mechanism, so --no-defaults is its complete ambient-config # exclusion while Oracle MySQL also needs --no-login-paths. client_args = [command, "--no-defaults"] if client_family == "mariadb": client_args.append("--ssl") else: client_args.extend(["--no-login-paths", "--ssl-mode", "REQUIRED"]) client_args.extend( [ "--default-character-set=utf8mb4", "--skip-reconnect", "-N", "-B", ] ) # --version and --help may exit successfully without rejecting unknown # options. A deliberate final unknown option proves that every preceding # safety option was parsed and accepted without opening a connection. option_probe_stop = "--nexuscrm-client-option-probe-stop" capability_result = subprocess.run( [*client_args, option_probe_stop], text=True, capture_output=True, check=False, env=probe_env, ) capability_text = f"{capability_result.stdout}\n{capability_result.stderr}" capability_diagnostics = [ line.strip() for line in capability_text.splitlines() if line.strip() ] if ( capability_result.returncode == 0 or len(capability_diagnostics) != 1 or option_probe_stop not in capability_diagnostics[0] ): raise ReconcileError("TiDB SQL transport client lacks required safety options") client_args.extend( [ f"-h{tidb_env['TIDB_HOST']}", f"-P{port}", f"-u{tidb_env['TIDB_USER']}", tidb_env["TIDB_DATABASE"], ] ) return client_args def _parse_sentinel(stdout: str, *, expected_updated_rows: int) -> dict[str, int]: matches = [line[len(SENTINEL) :] for line in stdout.splitlines() if line.startswith(SENTINEL)] if len(matches) != 1: raise ReconcileError("database reconcile did not emit exactly one verification result") try: raw = json.loads(matches[0]) except json.JSONDecodeError as exc: raise ReconcileError("database reconcile emitted invalid verification JSON") from exc expected_keys = { "guard_ok", "final_ok", "updated_rows", "expected_updated_rows", "target_count", } if not isinstance(raw, dict) or set(raw) != expected_keys: raise ReconcileError("database reconcile verification result has an invalid shape") numeric_keys = ( "guard_ok", "final_ok", "updated_rows", "expected_updated_rows", "target_count", ) if any(type(raw[key]) is not int for key in numeric_keys): raise ReconcileError("database reconcile verification values are not exact integers") parsed = {key: raw[key] for key in numeric_keys} if parsed["guard_ok"] != 1 or parsed["final_ok"] != 1: raise ReconcileError("database reconcile verification did not pass") if parsed["target_count"] != len(TARGET_DEVELOPER_NAMES): raise ReconcileError("database reconcile verification target count drifted") if ( parsed["expected_updated_rows"] != expected_updated_rows or parsed["updated_rows"] != expected_updated_rows ): raise ReconcileError("database reconcile updated-row count does not match the live snapshot") return parsed def _client_failure_detail(stderr: object, *, password: str) -> str: detail_lines = [line.strip() for line in str(stderr or "").splitlines() if line.strip()] detail = detail_lines[-1][:300] if detail_lines else "mysql client returned no diagnostic" if password: detail = detail.replace(password, "[REDACTED]") detail = re.sub(r"(?i)0x[0-9a-f]+", "[REDACTED_HEX]", detail) detail = re.sub( r"(?i)(? ReconcileResult: if execution_mode not in {"apply", "dry-run"}: raise ReconcileError(f"unsupported execution mode: {execution_mode!r}") client_env = { "PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin"), } if password: client_env["MYSQL_PWD"] = password else: client_env.pop("MYSQL_PWD", None) read_result = runner( list(client_args), input=render_live_read_sql(authority), text=True, capture_output=True, check=False, env=client_env, ) if read_result.returncode != 0: detail = _client_failure_detail(read_result.stderr, password=password) raise ReconcileError(f"database reconcile live read failed: {detail}") live_targets = parse_live_read(authority, str(read_result.stdout or "")) expected_updated_rows = sum( target.pre_state == "known_before" for target in live_targets ) mutation_result = runner( list(client_args), input=render_reconcile_sql( authority, live_targets, execution_mode=execution_mode, ), text=True, capture_output=True, check=False, env=client_env, ) if mutation_result.returncode != 0: detail = _client_failure_detail(mutation_result.stderr, password=password) raise ReconcileError(f"database reconcile failed before commit: {detail}") parsed_result = _parse_sentinel( str(mutation_result.stdout or ""), expected_updated_rows=expected_updated_rows, ) return ReconcileResult( targets=live_targets, updated_rows=parsed_result["updated_rows"], ) def build_evidence( authority: ReconcileAuthority, *, execution_mode: str, result: ReconcileResult, deployment_binding: DeploymentBinding, ) -> dict[str, Any]: updated_rows = result.updated_rows authorities = {target.developer_name: target for target in authority.targets} return { "format": EVIDENCE_FORMAT, "status": "passed", "execution_mode": execution_mode, "transaction_outcome": "committed" if execution_mode == "apply" else "rolled_back", "manifest_sha256": authority.manifest_sha256, "source_sha256": authority.source_sha256, "table_enum_or_id": "Quote", "owner_pack_state": "empty_string", "target_count": len(authority.targets), "reference_wrappers_per_target": len(EXPECTED_REFERENCES), "updated_rows": updated_rows, "already_converged_rows": len(authority.targets) - updated_rows, "deployment_binding": { "backup_reset_receipt_sha256": deployment_binding.backup_reset_receipt_sha256, "backup_reset_identity_sha256": deployment_binding.backup_reset_identity_sha256, "backup_reset_attempt_id": deployment_binding.backup_reset_attempt_id, "resolved_downstream_ref": deployment_binding.resolved_downstream_ref, "resolved_upstream_ref": deployment_binding.resolved_upstream_ref, "resolved_official_app_packs_ref": deployment_binding.resolved_official_app_packs_ref, "release_fingerprint_sha256": deployment_binding.release_fingerprint_sha256, }, "targets": [ { "developer_name": target.developer_name, "canonical_before_metadata_sha256": authorities[ target.developer_name ].canonical_before_sha256, "canonical_after_metadata_sha256": authorities[ target.developer_name ].canonical_after_sha256, "live_before_metadata_sha256": target.live_before_sha256, "live_after_metadata_sha256": target.live_after_sha256, "explicit_user_approver_count": target.explicit_user_approver_count, "pre_state": target.pre_state, "outcome": ( "already_target" if target.pre_state == "already_target" else "updated" if execution_mode == "apply" else "would_update" ), } for target in result.targets ], } def write_evidence(path: Path, evidence: dict[str, Any]) -> None: path = path.expanduser() if path.is_symlink(): raise ReconcileError(f"evidence path is a symlink: {path}") path = path.resolve() path.parent.mkdir(parents=True, exist_ok=True) if path.exists() and (path.is_symlink() or not path.is_file()): raise ReconcileError(f"evidence path is not a regular file: {path}") rendered = json.dumps(evidence, ensure_ascii=False, indent=2, sort_keys=True) + "\n" temp_path: Path | None = None try: with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False, ) as handle: handle.write(rendered) handle.flush() os.fsync(handle.fileno()) temp_path = Path(handle.name) os.replace(temp_path, path) except OSError as exc: if temp_path is not None: temp_path.unlink(missing_ok=True) raise ReconcileError(f"unable to write reconcile evidence: {path}") from exc def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source-root", required=True, type=Path) parser.add_argument("--manifest-file", type=Path, default=DEFAULT_MANIFEST) parser.add_argument("--evidence-file", required=True, type=Path) parser.add_argument("--execution-mode", required=True, choices=("dry-run", "apply")) parser.add_argument("--backup-reset-receipt-sha256", required=True) parser.add_argument("--backup-reset-identity-sha256", required=True) parser.add_argument("--backup-reset-attempt-id", required=True) parser.add_argument("--resolved-downstream-ref", required=True) parser.add_argument("--resolved-upstream-ref", required=True) parser.add_argument("--resolved-official-app-packs-ref", required=True) parser.add_argument("--release-fingerprint-sha256", required=True) return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(argv) try: tidb_env = { key: os.environ.get(key, "") for key in ( "TIDB_HOST", "TIDB_PORT", "TIDB_USER", "TIDB_PASSWORD", "TIDB_DATABASE", "TIDB_TLS", ) } authority = load_authority(args.source_root, args.manifest_file) deployment_binding = deployment_binding_from_args(args) result = execute_reconcile( authority, execution_mode=args.execution_mode, client_args=_mysql_client_args(tidb_env), password=tidb_env["TIDB_PASSWORD"], ) evidence = build_evidence( authority, execution_mode=args.execution_mode, result=result, deployment_binding=deployment_binding, ) write_evidence(args.evidence_file, evidence) print(json.dumps(evidence, ensure_ascii=False, sort_keys=True)) return 0 except ReconcileError as exc: print(f"Quote approval action-wrapper reconcile failed: {exc}", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())