From 223fa33f508d6df59c463ffe118ebf0170bfa44e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 29 Aug 2026 20:59:30 +0000 Subject: [PATCH] Phase 13.5: Persistent Host-SoT Patch Promotion Channel - promote-runner-patch, rollback-runner-patch (SHA-bound, atomic, FAIL CLOSED) --- .../control-plane/cp2a2_2b/rq-historical | 185 +++++++++++++++++- 1 file changed, 183 insertions(+), 2 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 05c2ff3..dc799d7 100644 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -156,6 +156,19 @@ RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt- CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7" CANDIDATE_TAG="historical-v2-historical-service:candidate-phase13-5-noop" +# ------------------------------------------------- PERSISTENT-HOST-SOT-PROMOTION +# Phase 13.5 · Persistent Host-SoT Patch Promotion Channel. KEIN echter Promote +# durch Rain. Nur run_backfill_year.py. Feste Identitaeten, KEIN User-Input. +# PATCH_SOURCE=/opt/data/run_backfill_year.py existiert NUR im RQ-Container +# (hermes-red-queen), nicht auf dem VPS-Host -> fester docker cp aus dem Container. +PATCH_SOURCE_CONTAINER="hermes-red-queen" +PATCH_SOURCE_PATH="/opt/data/run_backfill_year.py" +PATCH_TARGET="/opt/historical-v2/app/run_backfill_year.py" +# Read-only ermittelt (29.08.): SHA256 des von Red Queen geprueften Patch-Artefakts (8292 B). +EXPECTED_RUNNER_PATCH_SHA256="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524599f0752" +# Fester Backup-Root fuer die Promotion-Session (root-owned, nicht world-readable). +PROMOTE_BACKUP_ROOT="/opt/rq-historical-sot-backups/pre-runner-remediation" + # RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING RUN_MODE=MUTATING @@ -811,6 +824,172 @@ rollback_to_recovery_gold() { echo "ROLLBACK-TO-RECOVERY-GOLD OK. Container laeuft mit Recovery-Gold-Image." } +# ================= PERSISTENT-HOST-SOT-PROMOTION-CHANNEL ===================== +# Phase 13.5 · Persistent Host-SoT Patch Promotion. KEIN echter Promote durch Rain. +# Nur run_backfill_year.py. Feste Identitaeten, KEIN User-Input. + +# Patch aus dem RQ-Container in eine Temp-Datei holen (read-only, kein Target-Touch). +fetch_patch() { + # $1 = Ausgabe-Temp-Datei + docker cp "$PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH" "$1" >/dev/null 2>&1 || die "Patch aus $PATCH_SOURCE_CONTAINER nicht lesbar (FAIL CLOSED)" +} + +# Pre-Promotion Guards (Punkt 5). Read-only. FAIL CLOSED. +promote_pre_guards() { + echo " [guard A] Patch Source existiert als reguläre Datei (im RQ-Container):" + docker exec "$PATCH_SOURCE_CONTAINER" sh -c "[ -f '$PATCH_SOURCE_PATH' ]" >/dev/null 2>&1 || die "Guard A FAIL: Patch-Source fehlt (FAIL CLOSED)" + echo " OK" + echo " [guard B] kein Symlink (Source):" + docker exec "$PATCH_SOURCE_CONTAINER" sh -c "[ -L '$PATCH_SOURCE_PATH' ]" >/dev/null 2>&1 && die "Guard B FAIL: Source ist Symlink (FAIL CLOSED)" || echo " OK" + echo " [guard C] Source SHA == EXPECTED_RUNNER_PATCH_SHA256:" + local ps; ps="$(docker exec "$PATCH_SOURCE_CONTAINER" sha256sum "$PATCH_SOURCE_PATH" 2>/dev/null | awk '{print $1}')" + [ "$ps" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || die "Guard C FAIL: Source SHA $ps != erwartet (FAIL CLOSED)" + echo " OK ($ps)" + echo " [guard D] Target existiert als reguläre Datei:" + [ -f "$PATCH_TARGET" ] || die "Guard D FAIL: Target fehlt (FAIL CLOSED)" + echo " OK" + echo " [guard E] kein Symlink (Target):" + [ -L "$PATCH_TARGET" ] && die "Guard E FAIL: Target ist Symlink (FAIL CLOSED)" || echo " OK" + echo " [guard F] Host-SoT Baseline unverändert (Target-SHA == Gold-SHA 70d3abb1…):" + local ts; ts="$(sha256sum "$PATCH_TARGET" 2>/dev/null | awk '{print $1}')" + [ "$ts" = "70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" ] || die "Guard F FAIL: Target-SHA $ts != Gold-Baseline (FAIL CLOSED)" + echo " OK ($ts)" + echo " [guard G] Recovery-Gold Image inspectable:" + docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Guard G FAIL (FAIL CLOSED)" + echo " OK" + echo " [guard H] Recovery-Tag korrekt gebunden:" + local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)" + [ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "Guard H FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)" + echo " OK" + echo " [guard I] verify-recovery-gold-image PASS:" + ( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "Guard I FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)" + echo " [guard J] historical-db healthy:" + ensure_db_run && echo " OK" + echo " [guard K] kein Runner / kein Backfill aktiv:" + docker ps --format '{{.Names}}' | grep -qiE 'runner|backfill' && die "Guard K FAIL: Runner/Backfill aktiv (FAIL CLOSED)" || echo " OK" + echo " [guard L] Production Gates geschlossen:" + echo " OK (isolierter POC-Stack, kein Produktiv-Trading-Zugriff)" +} + +# Promotion-Session-Backup (Punkt 6). Root-owned, nicht world-readable. +promote_backup() { + # $1 = Session-Timestamp + local ts="$1" bdir bfile + bdir="$PROMOTE_BACKUP_ROOT/$ts" + mkdir -p "$bdir" || die "Backup-Verzeichnis nicht erstellbar (FAIL CLOSED)" + chmod 700 "$bdir" + bfile="$bdir/run_backfill_year.py" + cp -a "$PATCH_TARGET" "$bfile" || die "Backup fehlgeschlagen (FAIL CLOSED)" + chown root:root "$bfile"; chmod 600 "$bfile" + echo "$bfile" +} + +# Promotion-Session-Metadaten (Punkt 6/8). Root-owned. +promote_session_meta() { + # $1 = Session-Timestamp, $2 = Backup-Datei, $3 = TARGET_SHA_BEFORE + local ts="$1" bfile="$2" before="$3" mfile + mfile="$PROMOTE_BACKUP_ROOT/$ts/session.meta" + { + echo "SESSION_TS=$ts" + echo "PATCH_SOURCE=$PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH" + echo "PATCH_TARGET=$PATCH_TARGET" + echo "EXPECTED_RUNNER_PATCH_SHA256=$EXPECTED_RUNNER_PATCH_SHA256" + echo "TARGET_SHA_BEFORE=$before" + echo "BACKUP_SHA=$(sha256sum "$bfile" | awk '{print $1}')" + echo "TARGET_OWNER=$(stat -c '%U' "$PATCH_TARGET")" + echo "TARGET_GROUP=$(stat -c '%G' "$PATCH_TARGET")" + echo "TARGET_MODE=$(stat -c '%a' "$PATCH_TARGET")" + } > "$mfile" + chown root:root "$mfile"; chmod 600 "$mfile" +} + +# ---- promote-runner-patch (MUTATION; nur Red Queen fuehrt aus) ---- +promote_runner_patch() { + expect_zero "$@" + echo "=== PROMOTE-RUNNER-PATCH ($RUN_MODE) ===" + echo " SOURCE : $PATCH_SOURCE_CONTAINER:$PATCH_SOURCE_PATH" + echo " TARGET : $PATCH_TARGET" + echo " EXPECTED PATCH SHA : $EXPECTED_RUNNER_PATCH_SHA256" + local ts; ts="$(date +%Y%m%d_%H%M%S)" + echo " Pre-Guards:" + promote_pre_guards + local tbefore; tbefore="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')" + echo " CURRENT TARGET SHA : $tbefore" + echo " geplante Backup-Position : $PROMOTE_BACKUP_ROOT/$ts/run_backfill_year.py" + echo " geplante eine Datei-Mutation : $PATCH_TARGET" + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: wuerde Patch promoten (Backup + atomarer rename). KEINE Aktion." + exit 0 + fi + mode_or_die + # Backup + Session-Metadaten + local bfile; bfile="$(promote_backup "$ts")" + promote_session_meta "$ts" "$bfile" "$tbefore" + # Patch in Temp-Datei im Zielverzeichnis holen + SHA pruefen + local tmpf; tmpf="$PATCH_TARGET.tmp.$$" + fetch_patch "$tmpf" || { rm -f "$tmpf"; die "Promotion: Patch nicht lesbar (FAIL CLOSED)"; } + local ps; ps="$(sha256sum "$tmpf" | awk '{print $1}')" + [ "$ps" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || { rm -f "$tmpf"; die "Promotion: Temp-SHA $ps != erwartet (FAIL CLOSED)"; } + # Besitzer/Mode exakt wie bestehendes Target herstellen + chown "$(stat -c '%U' "$PATCH_TARGET"):$(stat -c '%G' "$PATCH_TARGET")" "$tmpf" + chmod "$(stat -c '%a' "$PATCH_TARGET")" "$tmpf" + # atomarer rename + mv -f "$tmpf" "$PATCH_TARGET" || { rm -f "$tmpf"; die "Promotion: atomarer rename fehlgeschlagen (FAIL CLOSED)"; } + echo " Promotion ausgeführt (Backup=$bfile)." + echo " Post-Promotion Verify:" + local tafter; tafter="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')" + echo " TARGET_SHA_AFTER : $tafter" + [ "$tafter" = "$EXPECTED_RUNNER_PATCH_SHA256" ] || die "Post-Promotion FAIL: Target-SHA != erwartet (FAIL CLOSED)" + echo " py_compile gegen persistenten Host-SoT Runner:" + python3 -m py_compile "$PATCH_TARGET" 2>&1 && echo " OK" || die "Post-Promotion FAIL: py_compile (FAIL CLOSED)" + echo "PROMOTE-RUNNER-PATCH OK. Host-SoT Runner ist jetzt der gepruefte Patch." +} + +# ---- rollback-runner-patch (MUTATION; nur Red Queen fuehrt aus) ---- +rollback_runner_patch() { + expect_zero "$@" + echo "=== ROLLBACK-RUNNER-PATCH ($RUN_MODE) ===" + echo " TARGET : $PATCH_TARGET" + echo " Rollback-Sicherheit (Punkt 11):" + # gueltige Promotion-Session vorhanden? + local latest; latest="$(ls -1 "$PROMOTE_BACKUP_ROOT" 2>/dev/null | sort | tail -1 || true)" + [ -n "$latest" ] || die "Rollback FAIL: keine gueltige Promotion-Session (FAIL CLOSED)" + local mfile bfile + mfile="$PROMOTE_BACKUP_ROOT/$latest/session.meta" + bfile="$PROMOTE_BACKUP_ROOT/$latest/run_backfill_year.py" + [ -f "$mfile" ] || die "Rollback FAIL: Session-Metadaten fehlen (FAIL CLOSED)" + [ -f "$bfile" ] || die "Rollback FAIL: Backup fehlt (FAIL CLOSED)" + [ -L "$bfile" ] && die "Rollback FAIL: Backup ist Symlink (FAIL CLOSED)" + local bsha; bsha="$(sha256sum "$bfile" | awk '{print $1}')" + local tbefore; tbefore="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')" + echo " Session : $latest" + echo " Backup SHA : $bsha" + echo " aktueller Target-Zustand : $tbefore" + echo " geplante eine Datei-Mutation : $PATCH_TARGET (zurueck auf Backup)" + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: wuerde Backup $bfile auf $PATCH_TARGET zurueckspielen. KEINE Aktion." + exit 0 + fi + mode_or_die + # aktuellen Target-Zustand dokumentieren (vor Rollback) + local cur; cur="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')" + echo " aktueller Target-SHA vor Rollback : $cur" + # Backup in Temp + SHA pruefen + atomarer rename + local tmpf; tmpf="$PATCH_TARGET.tmp.$$" + cp -a "$bfile" "$tmpf" || die "Rollback: Backup nicht lesbar (FAIL CLOSED)" + local ps; ps="$(sha256sum "$tmpf" | awk '{print $1}')" + [ "$ps" = "$bsha" ] || { rm -f "$tmpf"; die "Rollback: Temp-SHA != Backup-SHA (FAIL CLOSED)"; } + chown "$(stat -c '%U' "$PATCH_TARGET"):$(stat -c '%G' "$PATCH_TARGET")" "$tmpf" + chmod "$(stat -c '%a' "$PATCH_TARGET")" "$tmpf" + mv -f "$tmpf" "$PATCH_TARGET" || { rm -f "$tmpf"; die "Rollback: atomarer rename fehlgeschlagen (FAIL CLOSED)"; } + local tafter; tafter="$(sha256sum "$PATCH_TARGET" | awk '{print $1}')" + echo " TARGET_SHA_AFTER : $tafter" + # TARGET_SHA_BEFORE aus Session-Metadaten + local expected; expected="$(grep '^TARGET_SHA_BEFORE=' "$mfile" | cut -d= -f2)" + [ "$tafter" = "$expected" ] || die "Rollback FAIL: Target-SHA != TARGET_SHA_BEFORE (FAIL CLOSED)" + echo "ROLLBACK-RUNNER-PATCH OK. Target zurueck auf TARGET_SHA_BEFORE=$expected." +} + # ---- build-historical (NUR Build, kein recreate; DRY_RUN = kein Bau) ---- build_historical() { expect_zero "$@" @@ -921,7 +1100,7 @@ if [ "$SUBCMD" = "dry-run" ]; then SUBCMD="${1:-}" shift || true case "$SUBCMD" in - build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold) ;; + build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch) ;; *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;; esac fi @@ -1294,6 +1473,8 @@ case "$SUBCMD" in verify-candidate-image) verify_candidate_image "$@" ;; recreate-candidate-safe) recreate_candidate_safe "$@" ;; rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;; + promote-runner-patch) promote_runner_patch "$@" ;; + rollback-runner-patch) rollback_runner_patch "$@" ;; build-historical) build_historical "$@" ;; create-build-exclusion) expect_zero "$@" # Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen @@ -1321,6 +1502,6 @@ case "$SUBCMD" in recreate-historical) recreate_historical "$@" ;; verify-gold-reproduction) verify_gold_reproduction "$@" ;; - *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion" + *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion" ;; esac