From 8f29924ec656581bf1744255562679049afa3b4f Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Sun, 30 Aug 2026 09:26:59 +0000 Subject: [PATCH] Phase 13.5 Recovery Control-Plane Rotation: real recover freigegeben, Build-Context-Rotation (kein /opt/historical-v2), Recovery-Identity-Rotation (CURRENT_RECOVERY_ID statt d2db), READY-Gate, wrapper-sha --- .../control-plane/cp2a2_2b/rq-historical | 272 ++++++++++++++++-- 1 file changed, 246 insertions(+), 26 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index ce06762..7c9cc57 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -119,8 +119,12 @@ RECOVERY_GOLD_TAG="historical-v2-historical-service:recovery-gold-container-2026 # dediziert getaggtes Recovery-Gold-Image. KEIN latest, KEIN Recreate, KEIN # Runner, KEINE User-Args/Build-Args. Fester Context + Dockerfile + Tag. RECOVERY_REBUILD_TAG="historical-v2-historical-service:recovery-gold-rebuilt-20260829" -RECOVERY_BUILD_CONTEXT="/opt/historical-v2" -RECOVERY_BUILD_DOCKERFILE="/opt/historical-v2/Dockerfile" +# Build Context wird NICHT mehr statisch auf /opt/historical-v2 gesetzt. Er wird +# zur Laufzeit aus dem kontrollierten Control-Plane-State (RECOVERY_SOURCE_STATE) +# gelesen: GOLD_SOURCE_SESSION -> /opt/rq-historical-gold-sources/. +# Keine User-Argumente. FAIL CLOSED wenn State fehlt. +RECOVERY_BUILD_CONTEXT="" # zur Laufzeit aus State gesetzt +RECOVERY_BUILD_DOCKERFILE="" # zur Laufzeit aus State gesetzt # Critical Files (Punkt 11) mit CONSERVED GOLD SHA (fest, aus laufendem Container) declare -A CRITICAL_SHA=( ["run_backfill_year.py"]="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" @@ -148,8 +152,12 @@ declare -A CRITICAL_PATH=( # ------------------------------------------------- FINAL-REPRODUCTION-CONTROL-PLANE # Phase 13.5 · Safe Recreate + Rollback Preparation. KEIN echter Recreate/Rollback -# durch Rain. Recovery-Gold ist ab jetzt IMMUTABLE BASELINE (nur ROLLBACK/RECOVERY-BASE). -RECOVERY_GOLD_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2" +# Recovery-Gold ist ab jetzt IMMUTABLE BASELINE (nur ROLLBACK/RECOVERY-BASE). +# Die aktive Recovery-Image-ID wird NICHT statisch gesetzt, sondern zur Laufzeit +# aus dem kontrollierten Control-Plane-State (RECOVERY_IMAGE_STATE) gelesen. +# HISTORICAL_RECOVERY_IMAGE_ID (d2db...) bleibt NUR Evidence. FAIL CLOSED wenn +# State fehlt (kein freier Input, kein statischer Fallback). +RECOVERY_GOLD_IMAGE_ID="" # zur Laufzeit aus State gesetzt RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt-20260829" # Candidate Identity Audit (read-only, 29.08.): ausschliesslich ueber latest adressiert. # 0f6ca29a existiert NICHT mehr (historisch). Aktueller Candidate: @@ -187,6 +195,21 @@ declare -A GOLD_FORBIDDEN_EXTRA=( ["app/eligibility.py"]=1 ) +# ------------------------------------------------- RECOVERY CONTROL-PLANE STATE +# Phase 13.5 · Recovery Control-Plane Rotation. Dedizierter, root-owned State +# (KEIN latest, KEIN glob-first, KEINE Symlink-Semantik). Enthaelt die EINZIGE +# aktive Recovery-Gold-Source-Identitaet + die EINZIGE aktive Recovery-Image-ID. +# Kein freier Pfad. FAIL CLOSED wenn State fehlt/ungueltig. +CONTROL_PLANE_STATE_DIR="/opt/rq-historical-control-plane" +RECOVERY_SOURCE_STATE="$CONTROL_PLANE_STATE_DIR/recovery-gold-source.state" +RECOVERY_IMAGE_STATE="$CONTROL_PLANE_STATE_DIR/recovery-image.state" +# .dockerignore fester EXPECTED SHA (read-only ermittelt 30.08. gegen /opt/historical-v2; +# Inhalt exakt: app/app/ + app/eligibility.py). Kein Schätzen. +GOLD_DOCKERIGNORE_SHA="70bac51783f5ee956796cf2216ab17a550fcb646092edad3504763dbfabb881f" +# Historische Recovery-Image-ID (VERLOREN) — NUR Evidence, KEINE zwingende Output-ID. +HISTORICAL_RECOVERY_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2" + + # ------------------------------------------------- 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. @@ -233,10 +256,10 @@ PIN_REMEDIATION_NAME="rq-historical-pin-phase13-5-runner-remediation" # Unterordner je Rolle (Punkt 3) declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" ) # Archiv-Session-IDs: kurze Image-ID (), die den immutable -# Session-Ordner benennt. Recovery = bekannte Original-ID; Remediation = -# FULL_IMAGE_ID aus Build-Session (41e357a1..., aktuell im Store fehlend). -RECOVERY_ARCHIVE_ID="${RECOVERY_GOLD_IMAGE_ID#sha256:}" -RECOVERY_ARCHIVE_ID="${RECOVERY_ARCHIVE_ID:0:12}" +# Session-Ordner benennt. Recovery = aktive Image-ID aus Control-Plane-State +# (zur Laufzeit via load_recovery_state gesetzt); Remediation = FULL_IMAGE_ID +# aus Build-Session (41e357a1..., aktuell im Store fehlend). +RECOVERY_ARCHIVE_ID="" # zur Laufzeit aus State gesetzt (load_recovery_state) # Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...). REMEDIATION_ARCHIVE_ID="41e357a12b90" # Remediation-Archiv-Referenz (aus Build-Session falls vorhanden, sonst leer): @@ -329,6 +352,36 @@ exclusion_valid() { # .dockerignore exakt die 3 Legacy-Einträge, keine Gold-Da esac done < "$BUILD_EXCLUSION_FILE" } + +# ------------------------------------------------- RECOVERY CONTROL-PLANE STATE LOAD +# Laedt die EINZIGE aktive Recovery-Identitaet aus dem kontrollierten State +# (KEIN latest, KEIN glob-first, KEINE Symlink-Semantik). Setzt zur Laufzeit: +# RECOVERY_GOLD_IMAGE_ID (aus RECOVERY_IMAGE_STATE: NEW_RECOVERY_FULL_IMAGE_ID) +# RECOVERY_ARCHIVE_ID (abgeleitet aus der geladenen Image-ID) +# RECOVERY_BUILD_CONTEXT (aus RECOVERY_SOURCE_STATE: GOLD_SOURCE_SESSION) +# RECOVERY_BUILD_DOCKERFILE +# FAIL CLOSED wenn State fehlt/ungueltig. Kein freier Input, kein statischer Fallback. +load_recovery_state() { + # --- Recovery-Image-State (Punkt 17: NEW_RECOVERY_FULL_IMAGE_ID) --- + if [ ! -f "$RECOVERY_IMAGE_STATE" ]; then + die "load-recovery-state: Kein Recovery-Image-State ($RECOVERY_IMAGE_STATE). NUR nach erfolgreichem Recovery-Build (FAIL CLOSED)" + fi + local id + id="$(awk -F'=' '$1=="NEW_RECOVERY_FULL_IMAGE_ID"{print $2}' "$RECOVERY_IMAGE_STATE" 2>/dev/null)" + [[ "$id" =~ ^sha256:[a-f0-9]{64}$ ]] || die "load-recovery-state: NEW_RECOVERY_FULL_IMAGE_ID ungueltig/fehlt (FAIL CLOSED)" + RECOVERY_GOLD_IMAGE_ID="$id" + RECOVERY_ARCHIVE_ID="${id#sha256:}"; RECOVERY_ARCHIVE_ID="${RECOVERY_ARCHIVE_ID:0:12}" + # --- Recovery-Source-State (Punkt 9: GOLD_SOURCE_SESSION) --- + if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then + die "load-recovery-state: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)" + fi + local sess + sess="$(awk -F'=' '$1=="GOLD_SOURCE_SESSION"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)" + [[ "$sess" =~ ^[0-9]{8}_[0-9]{6}$ ]] || die "load-recovery-state: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)" + RECOVERY_BUILD_CONTEXT="$GOLD_SOURCE_ROOT/$sess" + RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile" +} + # Manifest einer Quelle (Gold-Container / neuer Temp-Container) erzeugen: # gibt zeilenweise 'sha app/' auf stdout; Quelle ist IMMER laufender Container gen_manifest() { # $1 = docker-Container-Name @@ -656,10 +709,17 @@ verify_image_config() { build_recovery_gold() { expect_zero "$@" echo "=== BUILD-RECOVERY-GOLD ($RUN_MODE) ===" - echo " Build Context : $RECOVERY_BUILD_CONTEXT (fix)" - echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (fix)" + # Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem State. + # Kein statisches /opt/historical-v2. Keine User-Argumente. + load_recovery_state + echo " Build Context : $RECOVERY_BUILD_CONTEXT (aus Control-Plane-State)" + echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (aus Control-Plane-State)" echo " Recovery-Tag : $RECOVERY_REBUILD_TAG (fix, direkt getaggt, KEIN latest)" echo " Guards:" + # Build Preflight (Punkt 12): verify-recovered-gold-build-source PASS zwingend. + echo " [preflight] verify-recovered-gold-build-source:" + verify_recovered_gold_build_source >/dev/null || die "build-recovery-gold: Recovery-Source nicht verifiziert (FAIL CLOSED)" + echo " OK" rebuild_guards # Build-Context-Manifest (Punkt 5) — read-only RECOVERY_GOLD_BUILD_INPUT echo " [build-input] RECOVERY_GOLD_BUILD_INPUT (Build-Context-Manifest):" @@ -743,6 +803,7 @@ EOF # Pre-Recreate/Pre-Rollback Gates (Punkt 10/16). Read-only. FAIL CLOSED. recreate_pre_gates() { + load_recovery_state echo " [gate A] verify-recovery-gold-image PASS:" ( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "Gate A FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)" echo " [gate B] verify-candidate-image PASS:" @@ -915,6 +976,7 @@ recreate_candidate_safe() { rollback_to_recovery_gold() { expect_zero "$@" echo "=== ROLLBACK-TO-RECOVERY-GOLD ($RUN_MODE) ===" + load_recovery_state echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEINE Volumes)" echo " Image : $RECOVERY_GOLD_TAG_FINAL -> $RECOVERY_GOLD_IMAGE_ID (immutable Baseline)" echo " Preconditions:" @@ -972,12 +1034,13 @@ promote_pre_guards() { 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 " [guard G] RECOVERY READY (verify-recovery-ready PASS):" + ( verify_ready_cmd recovery >/dev/null 2>&1 ) || die "Guard G FAIL: RECOVERY NOT READY (FAIL CLOSED)" echo " OK" - echo " [guard H] Recovery-Tag korrekt gebunden:" + echo " [guard H] Recovery-Tag korrekt gebunden (an aktive State-Image-ID):" + load_recovery_state 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)" + [ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "Guard H FAIL: Recovery-Tag != aktive 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)" @@ -1124,6 +1187,7 @@ remediation_full_image_id() { # FULL_IMAGE_ID aus Session (echo) — nur wenn r } # Pre-Build-Guards (Punkt 12: dry-run-Gates A–O). Read-only; alle FAIL CLOSED. remediation_guards() { + load_recovery_state echo " [guard A] Host-SoT vorhanden:" [ -f "/opt/historical-v2/app/run_backfill_year.py" ] || die "Guard A FAIL: Host-Runner fehlt (FAIL CLOSED)" echo " OK" @@ -1559,6 +1623,11 @@ verify_ready() { # $1 = role remediation) label="CANDIDATE READY" ;; esac echo "=== $label (READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY) ===" + # Recovery-Identitaet aus kontrolliertem State laden (Punkt 17/19). Kein + # statisches d2db, kein latest. FAIL CLOSED wenn State fehlt. + if [ "$role" = "recovery" ]; then + load_recovery_state + fi # IMAGE VERIFY (zwingend inspectable + korrekt) echo " [IMAGE] $(role_tag "$role"):" verify_image_present "$role" || die "READY-Gate FAIL: Image nicht inspectable (FAIL CLOSED)" @@ -1693,7 +1762,11 @@ archive_image() { # $1 = role local tag id subdir sdir tag="$(role_tag "$role")" case "$role" in - recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;; + recovery) + # Aktive Recovery-Identitaet aus kontrolliertem State (Punkt 19). Kein d2db. + load_recovery_state + id="$RECOVERY_GOLD_IMAGE_ID" + ;; remediation) remediation_session || die "archive(remediation): keine gueltige Build-Session (FAIL CLOSED)" id="$(remediation_full_image_id)" @@ -1767,7 +1840,11 @@ pin_image() { # $1 = role name="$(role_pin "$role")" tag="$(role_tag "$role")" case "$role" in - recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;; + recovery) + # Aktive Recovery-Identitaet aus kontrolliertem State (Punkt 19). Kein d2db. + load_recovery_state + id="$RECOVERY_GOLD_IMAGE_ID" + ;; remediation) remediation_session || die "pin(remediation): keine gueltige Build-Session (FAIL CLOSED)" id="$(remediation_full_image_id)" @@ -1808,12 +1885,14 @@ pin_image() { # $1 = role verify_pin_cmd() { # $1 = role (read-only) local role="$1"; expect_zero "${@:2}" echo "=== VERIFY-PIN($role) ===" + if [ "$role" = "recovery" ]; then load_recovery_state; fi if verify_pin "$role"; then echo "VERIFY-PIN($role): PASS"; exit 0 else die "VERIFY-PIN($role): FAIL (FAIL CLOSED)"; fi } verify_archive_cmd() { # $1 = role (read-only) local role="$1"; expect_zero "${@:2}" echo "=== VERIFY-ARCHIVE($role) ===" + if [ "$role" = "recovery" ]; then load_recovery_state; fi verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)" exit 0 } @@ -1827,6 +1906,7 @@ verify_ready_cmd() { # $1 = role (read-only) restore_recovery_dry() { # dry-run expect_zero "$@" echo "=== RESTORE-RECOVERY-IMAGE-ARCHIVE (DRY-RUN — DESIGN; kein echter Restore) ===" + load_recovery_state local sdir sid base img sdir="$(role_arch_dir recovery)" sid="$RECOVERY_ARCHIVE_ID" @@ -2001,25 +2081,164 @@ recover_gold_build_source_dry() { } # recover-gold-build-source (Dispatcher): dry-run zeigt Plan (ZERO MUTATION), -# real ist in dieser Mission BLOCKIERT (FAIL CLOSED). +# real fuehrt den kontrollierten Workflow aus (Punkt 3/4/5/8/9). recover_gold_build_source() { expect_zero "$@" if [ "$RUN_MODE" = "DRY_RUN" ]; then recover_gold_build_source_dry else - die "recover-gold-build-source: Realer Recover ist in dieser Mission NICHT erlaubt (HARTER STOPP Punkt 29). NUR dry-run (FAIL CLOSED)" + recover_gold_build_source_real fi } -# verify-recovered-gold-build-source (read-only): prueft eine bestehende Recovery-Source. +# Realer Recover (Punkt 3/4/5/8/9): materialisiert die Gold-Source aus dem +# bewiesenen DEPLOYED GOLD CONTAINER in eine immutable Session. Keine freien +# Parameter. FAIL CLOSED bei jeder Abweichung. ZERO MUTATION ausser der Session. +recover_gold_build_source_real() { + expect_zero "$@" + echo "=== RECOVER-GOLD-BUILD-SOURCE (real) ===" + # Punkt 4: TOCTOU vermeiden — unmittelbar vor Extraktion erneut verifizieren. + echo " [verify] verify-deployed-gold-source (TOCTOU-Guard):" + verify_deployed_gold_source >/dev/null || die "recover: verify-deployed-gold-source FAIL (FAIL CLOSED)" + echo " OK" + # Punkt 8: immutable Session — Root darf nicht existieren (FAIL CLOSED). + if [ -e "$GOLD_SOURCE_ROOT" ]; then + die "recover: Gold-Source-Root existiert bereits $GOLD_SOURCE_ROOT (KEIN Overwrite, FAIL CLOSED)" + fi + local sess + sess="$(date +%Y%m%d_%H%M%S)" + local sdir="$GOLD_SOURCE_ROOT/$sess" + # Punkt 5: Application Tree ausschliesslich aus dem hardcoded Gold-Container. + # Keine Host-Application-Dateien, keine SOT-Session, kein latest, keine freien docker-cp-Inputs. + mkdir -p "$sdir/app" || die "recover: Session-Ordner nicht anlegbar (FAIL CLOSED)" + echo " [extract] docker cp $EXPECTED_GOLD_SOURCE_CID:$GOLD_SOURCE_APP_PREFIX/. -> $sdir/app/" + if ! docker cp "$EXPECTED_GOLD_SOURCE_CID:$GOLD_SOURCE_APP_PREFIX/." "$sdir/app/" >/dev/null 2>&1; then + rm -rf "$GOLD_SOURCE_ROOT"; die "recover: docker cp Application-Tree fehlgeschlagen (FAIL CLOSED)" + fi + # Punkt 6: Build Support separat aus fest SHA-gebundenen Quellen (Host/SoT-Bestand). + echo " [build-support] SHA-gebundene Host-Dateien:" + local key spec src exp s + for key in "${!GOLD_BUILD_SUPPORT[@]}"; do + spec="${GOLD_BUILD_SUPPORT[$key]}"; src="${spec%%|*}"; exp="${spec##*|}" + [ -f "$src" ] || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: Build-Support fehlt $src (FAIL CLOSED)"; } + s="$(sha256sum "$src" | awk '{print $1}')" + [ "$s" = "$exp" ] || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: Build-Support SHA mismatch $key (FAIL CLOSED)"; } + cp -a "$src" "$sdir/$(basename "$src")" || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: Build-Support kopieren fehlgeschlagen (FAIL CLOSED)"; } + echo " $key == $exp" + done + # Punkt 6: .dockerignore kontrolliert erzeugen (exakt die Legacy-Exclusions). + printf 'app/app/\napp/eligibility.py\n' > "$sdir/.dockerignore" + local dis + dis="$(sha256sum "$sdir/.dockerignore" | awk '{print $1}')" + [ "$dis" = "$GOLD_DOCKERIGNORE_SHA" ] || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: .dockerignore SHA mismatch (FAIL CLOSED)"; } + echo " .dockerignore == $GOLD_DOCKERIGNORE_SHA" + # Punkt 8: Application Manifest (63/63 gegen Gold) + Build-Support Manifest. + ( cd "$sdir/app" && find . -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \ + | while IFS= read -r rel; do [ -f "$rel" ] && sha256sum "$rel"; done | awk '{print $1" app/"$2}' ) > "$sdir/application.manifest" + local match=0 diverged=0 missing=0 + local -A gsha + while read -r sha p; do gsha["$p"]="$sha"; done < "$GOLD_MANIFEST" + local path g c + for path in "${!gsha[@]}"; do + g="${gsha[$path]}" + c="$(awk -v r="$path" '$2==r{print $1}' "$sdir/application.manifest" 2>/dev/null)" + if [ -z "$c" ]; then missing=$((missing+1)); + elif [ "$c" = "$g" ]; then match=$((match+1)); + else diverged=$((diverged+1)); fi + done + echo " [application] MATCH=$match DIVERGED=$diverged MISSING=$missing" + [ "$match" = "63" ] && [ "$diverged" = "0" ] && [ "$missing" = "0" ] || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: Application != Gold (FAIL CLOSED)"; } + # Build-Support Manifest (Punkt 8) + { + for key in "${!GOLD_BUILD_SUPPORT[@]}"; do + spec="${GOLD_BUILD_SUPPORT[$key]}"; src="${spec%%|*}"; exp="${spec##*|}" + echo "$key $exp" + done + echo ".dockerignore $GOLD_DOCKERIGNORE_SHA" + } > "$sdir/build-support.manifest" + # Punkt 13: FINAL_BUILD_INPUT_SHA (Build-Input-Identitaet) berechnen. + local final_sha + final_sha="$( ( cd "$sdir" && find . -type f ! -name 'session.meta' ! -name 'application.manifest' ! -name 'build-support.manifest' ! -name 'final-build-input.manifest' -printf '%P\n' 2>/dev/null | sort | while IFS= read -r f; do [ -f "$f" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )" + echo "$final_sha" > "$sdir/final-build-input.manifest" + # Punkt 8: session.meta + { + echo "GOLD_SOURCE_SESSION=$sess" + echo "SOURCE_CONTAINER_ID=$EXPECTED_GOLD_SOURCE_CID" + echo "SOURCE_IMAGE_ID=$EXPECTED_GOLD_SOURCE_IMAGE_ID" + echo "CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "FINAL_BUILD_INPUT_SHA=$final_sha" + } > "$sdir/session.meta" + # Punkt 9: aktive Recovery-Gold-Source-Identitaet kontrolliert speichern. + mkdir -p "$CONTROL_PLANE_STATE_DIR" || { rm -rf "$GOLD_SOURCE_ROOT"; die "recover: State-Dir nicht anlegbar (FAIL CLOSED)"; } + if [ -e "$RECOVERY_SOURCE_STATE" ]; then + rm -rf "$GOLD_SOURCE_ROOT"; die "recover: Recovery-Source-State existiert bereits (KEIN Overwrite, FAIL CLOSED)" + fi + { + echo "GOLD_SOURCE_SESSION=$sess" + echo "FINAL_BUILD_INPUT_SHA=$final_sha" + echo "SOURCE_CONTAINER_ID=$EXPECTED_GOLD_SOURCE_CID" + echo "SOURCE_IMAGE_ID=$EXPECTED_GOLD_SOURCE_IMAGE_ID" + echo "CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + } > "$RECOVERY_SOURCE_STATE" + chown root:root "$RECOVERY_SOURCE_STATE"; chmod 600 "$RECOVERY_SOURCE_STATE" + # Punkt 8: immutable klassifizieren (root-owned, nicht world-readable). + chown -R root:root "$sdir"; chmod -R 700 "$sdir" + echo "RECOVER-GOLD-BUILD-SOURCE OK: $sdir (immutable, 63/63, Build-Support SHA-gebunden)" + echo " State: $RECOVERY_SOURCE_STATE" + echo " FINAL_BUILD_INPUT_SHA=$final_sha" +} + +# verify-recovered-gold-build-source (read-only): prueft eine bestehende Recovery-Source +# gegen die EXAKTE Session-Identitaet aus dem Control-Plane-State (Punkt 10). +# NICHT chronologisch newest, NICHT glob-first, NICHT latest. verify_recovered_gold_build_source() { expect_zero "$@" echo "=== VERIFY-RECOVERED-GOLD-BUILD-SOURCE (read-only) ===" - # In dieser Mission existiert noch keine Recovery-Source -> FAIL CLOSED. - if [ ! -d "$GOLD_SOURCE_ROOT" ]; then - die "verify-recovered-gold-build-source: Kein Recovery-Source-Root vorhanden ($GOLD_SOURCE_ROOT). NUR nach realem recover (separate Mission)." + # Exakte Session-Identitaet aus State (Punkt 9/10). Kein latest/glob-first. + if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then + die "verify-recovered-gold-build-source: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)" fi - echo "VERDICT: RECOVERED GOLD BUILD SOURCE OK" + local sess + sess="$(awk -F'=' '$1=="GOLD_SOURCE_SESSION"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)" + [[ "$sess" =~ ^[0-9]{8}_[0-9]{6}$ ]] || die "verify-recovered-gold-build-source: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)" + local sdir="$GOLD_SOURCE_ROOT/$sess" + [ -d "$sdir" ] || die "verify-recovered-gold-build-source: Session-Ordner fehlt $sdir (FAIL CLOSED)" + # Pflichtdateien der immutable Session (Punkt 8) + local f + for f in app Dockerfile .dockerignore requirements.txt session.meta application.manifest build-support.manifest final-build-input.manifest; do + [ -e "$sdir/$f" ] || die "verify-recovered-gold-build-source: fehlt $sdir/$f (FAIL CLOSED)" + done + # Application Manifest (Punkt 8): 63/63 gegen Gold + echo " [application] $sdir/application.manifest vs Gold (63/63):" + local match=0 diverged=0 missing=0 + local -A gsha + while read -r sha p; do gsha["$p"]="$sha"; done < "$GOLD_MANIFEST" + local path g s + for path in "${!gsha[@]}"; do + g="${gsha[$path]}" + s="$(awk -v r="$path" '$2==r{print $1}' "$sdir/application.manifest" 2>/dev/null)" + if [ -z "$s" ]; then missing=$((missing+1)); + elif [ "$s" = "$g" ]; then match=$((match+1)); + else diverged=$((diverged+1)); fi + done + echo " MATCH=$match DIVERGED=$diverged MISSING=$missing" + [ "$match" = "63" ] && [ "$diverged" = "0" ] && [ "$missing" = "0" ] || die "verify-recovered-gold-build-source: Application != Gold (FAIL CLOSED)" + # Build-Support SHAs (Punkt 6): fest SHA-gebunden + echo " [build-support] SHA-Bindings:" + gold_build_support_ok || die "verify-recovered-gold-build-source: Build-Support SHA mismatch (FAIL CLOSED)" + # .dockerignore fester EXPECTED SHA (Punkt 6) + local dis + dis="$(sha256sum "$sdir/.dockerignore" 2>/dev/null | awk '{print $1}')" + [ "$dis" = "$GOLD_DOCKERIGNORE_SHA" ] || die "verify-recovered-gold-build-source: .dockerignore SHA $dis != erwartet (FAIL CLOSED)" + echo " .dockerignore == $GOLD_DOCKERIGNORE_SHA" + # Build-Input-Identitaet (Punkt 13): FINAL_BUILD_INPUT_SHA aus State pruefen + local want got + want="$(awk -F'=' '$1=="FINAL_BUILD_INPUT_SHA"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)" + [ -n "$want" ] && [[ "$want" =~ ^[a-f0-9]{64}$ ]] || die "verify-recovered-gold-build-source: FINAL_BUILD_INPUT_SHA fehlt/ungueltig (FAIL CLOSED)" + got="$( ( cd "$sdir" && find . -type f ! -name 'session.meta' ! -name 'application.manifest' ! -name 'build-support.manifest' ! -name 'final-build-input.manifest' -printf '%P\n' 2>/dev/null | sort | while IFS= read -r f; do [ -f "$f" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )" + [ "$got" = "$want" ] || die "verify-recovered-gold-build-source: Build-Input SHA mismatch (Source veraendert) (FAIL CLOSED)" + echo " FINAL_BUILD_INPUT_SHA == $want" + echo "VERDICT: RECOVERED GOLD BUILD SOURCE OK (Session $sess, 63/63, Build-Support + .dockerignore + Build-Input SHA)" exit 0 } @@ -2454,9 +2673,10 @@ case "$SUBCMD" in # Gold-Source fest an Container+Image+63-File-Manifest gebunden. KEIN latest, # KEIN full_sot_backup(), KEINE freien Inputs. Nur Source-Recovery, KEIN Docker-Build. # verify-* read-only. Realer recover in dieser Mission BLOCKIERT (nur dry-run). - verify-deployed-gold-source) verify_deployed_gold_source "$@" ;; - recover-gold-build-source) recover_gold_build_source "$@" ;; - verify-recovered-gold-build-source) verify_recovered_gold_build_source "$@" ;; + verify-deployed-gold-source) verify_deployed_gold_source "$@";; + recover-gold-build-source) recover_gold_build_source "$@";; + verify-recovered-gold-build-source) verify_recovered_gold_build_source "$@";; + wrapper-sha) expect_zero "$@"; sha256sum /usr/local/bin/rq-historical ;; *) 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-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|verify-recovery-archive|verify-remediation-archive|verify-recovery-pin|verify-remediation-pin|verify-recovery-ready|verify-remediation-ready|dry-restore-recovery-image-archive|restore-recovery-image-archive|verify-deployed-gold-source|dry-run recover-gold-build-source|recover-gold-build-source|verify-recovered-gold-build-source" ;;