Phase 13.5: Persistent Host-SoT Patch Promotion Channel - promote-runner-patch, rollback-runner-patch (SHA-bound, atomic, FAIL CLOSED)

This commit is contained in:
root 2026-08-29 20:59:30 +00:00
parent 6bcad03e18
commit 223fa33f50

View file

@ -156,6 +156,19 @@ RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt-
CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7" CANDIDATE_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7"
CANDIDATE_TAG="historical-v2-historical-service:candidate-phase13-5-noop" 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 (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
RUN_MODE=MUTATING RUN_MODE=MUTATING
@ -811,6 +824,172 @@ rollback_to_recovery_gold() {
echo "ROLLBACK-TO-RECOVERY-GOLD OK. Container laeuft mit Recovery-Gold-Image." 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 (NUR Build, kein recreate; DRY_RUN = kein Bau) ----
build_historical() { build_historical() {
expect_zero "$@" expect_zero "$@"
@ -921,7 +1100,7 @@ if [ "$SUBCMD" = "dry-run" ]; then
SUBCMD="${1:-}" SUBCMD="${1:-}"
shift || true shift || true
case "$SUBCMD" in 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" ;; *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold zulaessig" ;;
esac esac
fi fi
@ -1294,6 +1473,8 @@ case "$SUBCMD" in
verify-candidate-image) verify_candidate_image "$@" ;; verify-candidate-image) verify_candidate_image "$@" ;;
recreate-candidate-safe) recreate_candidate_safe "$@" ;; recreate-candidate-safe) recreate_candidate_safe "$@" ;;
rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;; rollback-to-recovery-gold) rollback_to_recovery_gold "$@" ;;
promote-runner-patch) promote_runner_patch "$@" ;;
rollback-runner-patch) rollback_runner_patch "$@" ;;
build-historical) build_historical "$@" ;; build-historical) build_historical "$@" ;;
create-build-exclusion) expect_zero "$@" create-build-exclusion) expect_zero "$@"
# Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen # Build-Pollution-Exclusion implementieren (Punkt 5-8): .dockerignore anlegen/ersetzen
@ -1321,6 +1502,6 @@ case "$SUBCMD" in
recreate-historical) recreate_historical "$@" ;; recreate-historical) recreate_historical "$@" ;;
verify-gold-reproduction) verify_gold_reproduction "$@" ;; verify-gold-reproduction) verify_gold_reproduction "$@" ;;
*) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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 <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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 esac