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

This commit is contained in:
Rain Ocampo 2026-08-30 09:26:59 +00:00
parent 563ed111b9
commit 8f29924ec6

View file

@ -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 # dediziert getaggtes Recovery-Gold-Image. KEIN latest, KEIN Recreate, KEIN
# Runner, KEINE User-Args/Build-Args. Fester Context + Dockerfile + Tag. # Runner, KEINE User-Args/Build-Args. Fester Context + Dockerfile + Tag.
RECOVERY_REBUILD_TAG="historical-v2-historical-service:recovery-gold-rebuilt-20260829" RECOVERY_REBUILD_TAG="historical-v2-historical-service:recovery-gold-rebuilt-20260829"
RECOVERY_BUILD_CONTEXT="/opt/historical-v2" # Build Context wird NICHT mehr statisch auf /opt/historical-v2 gesetzt. Er wird
RECOVERY_BUILD_DOCKERFILE="/opt/historical-v2/Dockerfile" # zur Laufzeit aus dem kontrollierten Control-Plane-State (RECOVERY_SOURCE_STATE)
# gelesen: GOLD_SOURCE_SESSION -> /opt/rq-historical-gold-sources/<SESSION>.
# 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) # Critical Files (Punkt 11) mit CONSERVED GOLD SHA (fest, aus laufendem Container)
declare -A CRITICAL_SHA=( declare -A CRITICAL_SHA=(
["run_backfill_year.py"]="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" ["run_backfill_year.py"]="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20"
@ -148,8 +152,12 @@ declare -A CRITICAL_PATH=(
# ------------------------------------------------- FINAL-REPRODUCTION-CONTROL-PLANE # ------------------------------------------------- FINAL-REPRODUCTION-CONTROL-PLANE
# Phase 13.5 · Safe Recreate + Rollback Preparation. KEIN echter Recreate/Rollback # 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 ist ab jetzt IMMUTABLE BASELINE (nur ROLLBACK/RECOVERY-BASE).
RECOVERY_GOLD_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2" # 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" RECOVERY_GOLD_TAG_FINAL="historical-v2-historical-service:recovery-gold-rebuilt-20260829"
# Candidate Identity Audit (read-only, 29.08.): ausschliesslich ueber latest adressiert. # Candidate Identity Audit (read-only, 29.08.): ausschliesslich ueber latest adressiert.
# 0f6ca29a existiert NICHT mehr (historisch). Aktueller Candidate: # 0f6ca29a existiert NICHT mehr (historisch). Aktueller Candidate:
@ -187,6 +195,21 @@ declare -A GOLD_FORBIDDEN_EXTRA=(
["app/eligibility.py"]=1 ["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 # ------------------------------------------------- PERSISTENT-HOST-SOT-PROMOTION
# Phase 13.5 · Persistent Host-SoT Patch Promotion Channel. KEIN echter Promote # Phase 13.5 · Persistent Host-SoT Patch Promotion Channel. KEIN echter Promote
# durch Rain. Nur run_backfill_year.py. Feste Identitaeten, KEIN User-Input. # 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) # Unterordner je Rolle (Punkt 3)
declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" ) declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" )
# Archiv-Session-IDs: kurze Image-ID (<IMAGE_ID_SHORT>), die den immutable # Archiv-Session-IDs: kurze Image-ID (<IMAGE_ID_SHORT>), die den immutable
# Session-Ordner benennt. Recovery = bekannte Original-ID; Remediation = # Session-Ordner benennt. Recovery = aktive Image-ID aus Control-Plane-State
# FULL_IMAGE_ID aus Build-Session (41e357a1..., aktuell im Store fehlend). # (zur Laufzeit via load_recovery_state gesetzt); Remediation = FULL_IMAGE_ID
RECOVERY_ARCHIVE_ID="${RECOVERY_GOLD_IMAGE_ID#sha256:}" # aus Build-Session (41e357a1..., aktuell im Store fehlend).
RECOVERY_ARCHIVE_ID="${RECOVERY_ARCHIVE_ID:0:12}" RECOVERY_ARCHIVE_ID="" # zur Laufzeit aus State gesetzt (load_recovery_state)
# Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...). # Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...).
REMEDIATION_ARCHIVE_ID="41e357a12b90" REMEDIATION_ARCHIVE_ID="41e357a12b90"
# Remediation-Archiv-Referenz (aus Build-Session falls vorhanden, sonst leer): # 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 esac
done < "$BUILD_EXCLUSION_FILE" 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: # Manifest einer Quelle (Gold-Container / neuer Temp-Container) erzeugen:
# gibt zeilenweise 'sha app/<rel>' auf stdout; Quelle ist IMMER laufender Container # gibt zeilenweise 'sha app/<rel>' auf stdout; Quelle ist IMMER laufender Container
gen_manifest() { # $1 = docker-Container-Name gen_manifest() { # $1 = docker-Container-Name
@ -656,10 +709,17 @@ verify_image_config() {
build_recovery_gold() { build_recovery_gold() {
expect_zero "$@" expect_zero "$@"
echo "=== BUILD-RECOVERY-GOLD ($RUN_MODE) ===" echo "=== BUILD-RECOVERY-GOLD ($RUN_MODE) ==="
echo " Build Context : $RECOVERY_BUILD_CONTEXT (fix)" # Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem State.
echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (fix)" # 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 " Recovery-Tag : $RECOVERY_REBUILD_TAG (fix, direkt getaggt, KEIN latest)"
echo " Guards:" 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 rebuild_guards
# Build-Context-Manifest (Punkt 5) — read-only RECOVERY_GOLD_BUILD_INPUT # Build-Context-Manifest (Punkt 5) — read-only RECOVERY_GOLD_BUILD_INPUT
echo " [build-input] RECOVERY_GOLD_BUILD_INPUT (Build-Context-Manifest):" 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. # Pre-Recreate/Pre-Rollback Gates (Punkt 10/16). Read-only. FAIL CLOSED.
recreate_pre_gates() { recreate_pre_gates() {
load_recovery_state
echo " [gate A] verify-recovery-gold-image PASS:" 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)" ( 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:" echo " [gate B] verify-candidate-image PASS:"
@ -915,6 +976,7 @@ recreate_candidate_safe() {
rollback_to_recovery_gold() { rollback_to_recovery_gold() {
expect_zero "$@" expect_zero "$@"
echo "=== ROLLBACK-TO-RECOVERY-GOLD ($RUN_MODE) ===" echo "=== ROLLBACK-TO-RECOVERY-GOLD ($RUN_MODE) ==="
load_recovery_state
echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEINE Volumes)" echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEINE Volumes)"
echo " Image : $RECOVERY_GOLD_TAG_FINAL -> $RECOVERY_GOLD_IMAGE_ID (immutable Baseline)" echo " Image : $RECOVERY_GOLD_TAG_FINAL -> $RECOVERY_GOLD_IMAGE_ID (immutable Baseline)"
echo " Preconditions:" echo " Preconditions:"
@ -972,12 +1034,13 @@ promote_pre_guards() {
local ts; ts="$(sha256sum "$PATCH_TARGET" 2>/dev/null | awk '{print $1}')" 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)" [ "$ts" = "70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" ] || die "Guard F FAIL: Target-SHA $ts != Gold-Baseline (FAIL CLOSED)"
echo " OK ($ts)" echo " OK ($ts)"
echo " [guard G] Recovery-Gold Image inspectable:" echo " [guard G] RECOVERY READY (verify-recovery-ready PASS):"
docker image inspect "$RECOVERY_GOLD_IMAGE_ID" >/dev/null 2>&1 || die "Guard G FAIL (FAIL CLOSED)" ( verify_ready_cmd recovery >/dev/null 2>&1 ) || die "Guard G FAIL: RECOVERY NOT READY (FAIL CLOSED)"
echo " OK" 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)" 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 " OK"
echo " [guard I] verify-recovery-gold-image PASS:" 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)" ( 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 AO). Read-only; alle FAIL CLOSED. # Pre-Build-Guards (Punkt 12: dry-run-Gates AO). Read-only; alle FAIL CLOSED.
remediation_guards() { remediation_guards() {
load_recovery_state
echo " [guard A] Host-SoT vorhanden:" echo " [guard A] Host-SoT vorhanden:"
[ -f "/opt/historical-v2/app/run_backfill_year.py" ] || die "Guard A FAIL: Host-Runner fehlt (FAIL CLOSED)" [ -f "/opt/historical-v2/app/run_backfill_year.py" ] || die "Guard A FAIL: Host-Runner fehlt (FAIL CLOSED)"
echo " OK" echo " OK"
@ -1559,6 +1623,11 @@ verify_ready() { # $1 = role
remediation) label="CANDIDATE READY" ;; remediation) label="CANDIDATE READY" ;;
esac esac
echo "=== $label (READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY) ===" 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) # IMAGE VERIFY (zwingend inspectable + korrekt)
echo " [IMAGE] $(role_tag "$role"):" echo " [IMAGE] $(role_tag "$role"):"
verify_image_present "$role" || die "READY-Gate FAIL: Image nicht inspectable (FAIL CLOSED)" 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 local tag id subdir sdir
tag="$(role_tag "$role")" tag="$(role_tag "$role")"
case "$role" in 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)
remediation_session || die "archive(remediation): keine gueltige Build-Session (FAIL CLOSED)" remediation_session || die "archive(remediation): keine gueltige Build-Session (FAIL CLOSED)"
id="$(remediation_full_image_id)" id="$(remediation_full_image_id)"
@ -1767,7 +1840,11 @@ pin_image() { # $1 = role
name="$(role_pin "$role")" name="$(role_pin "$role")"
tag="$(role_tag "$role")" tag="$(role_tag "$role")"
case "$role" in 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)
remediation_session || die "pin(remediation): keine gueltige Build-Session (FAIL CLOSED)" remediation_session || die "pin(remediation): keine gueltige Build-Session (FAIL CLOSED)"
id="$(remediation_full_image_id)" id="$(remediation_full_image_id)"
@ -1808,12 +1885,14 @@ pin_image() { # $1 = role
verify_pin_cmd() { # $1 = role (read-only) verify_pin_cmd() { # $1 = role (read-only)
local role="$1"; expect_zero "${@:2}" local role="$1"; expect_zero "${@:2}"
echo "=== VERIFY-PIN($role) ===" echo "=== VERIFY-PIN($role) ==="
if [ "$role" = "recovery" ]; then load_recovery_state; fi
if verify_pin "$role"; then echo "VERIFY-PIN($role): PASS"; exit 0 if verify_pin "$role"; then echo "VERIFY-PIN($role): PASS"; exit 0
else die "VERIFY-PIN($role): FAIL (FAIL CLOSED)"; fi else die "VERIFY-PIN($role): FAIL (FAIL CLOSED)"; fi
} }
verify_archive_cmd() { # $1 = role (read-only) verify_archive_cmd() { # $1 = role (read-only)
local role="$1"; expect_zero "${@:2}" local role="$1"; expect_zero "${@:2}"
echo "=== VERIFY-ARCHIVE($role) ===" echo "=== VERIFY-ARCHIVE($role) ==="
if [ "$role" = "recovery" ]; then load_recovery_state; fi
verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)" verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)"
exit 0 exit 0
} }
@ -1827,6 +1906,7 @@ verify_ready_cmd() { # $1 = role (read-only)
restore_recovery_dry() { # dry-run restore_recovery_dry() { # dry-run
expect_zero "$@" expect_zero "$@"
echo "=== RESTORE-RECOVERY-IMAGE-ARCHIVE (DRY-RUN — DESIGN; kein echter Restore) ===" echo "=== RESTORE-RECOVERY-IMAGE-ARCHIVE (DRY-RUN — DESIGN; kein echter Restore) ==="
load_recovery_state
local sdir sid base img local sdir sid base img
sdir="$(role_arch_dir recovery)" sdir="$(role_arch_dir recovery)"
sid="$RECOVERY_ARCHIVE_ID" sid="$RECOVERY_ARCHIVE_ID"
@ -2001,25 +2081,164 @@ recover_gold_build_source_dry() {
} }
# recover-gold-build-source (Dispatcher): dry-run zeigt Plan (ZERO MUTATION), # 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() { recover_gold_build_source() {
expect_zero "$@" expect_zero "$@"
if [ "$RUN_MODE" = "DRY_RUN" ]; then if [ "$RUN_MODE" = "DRY_RUN" ]; then
recover_gold_build_source_dry recover_gold_build_source_dry
else 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 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() { verify_recovered_gold_build_source() {
expect_zero "$@" expect_zero "$@"
echo "=== VERIFY-RECOVERED-GOLD-BUILD-SOURCE (read-only) ===" echo "=== VERIFY-RECOVERED-GOLD-BUILD-SOURCE (read-only) ==="
# In dieser Mission existiert noch keine Recovery-Source -> FAIL CLOSED. # Exakte Session-Identitaet aus State (Punkt 9/10). Kein latest/glob-first.
if [ ! -d "$GOLD_SOURCE_ROOT" ]; then if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then
die "verify-recovered-gold-build-source: Kein Recovery-Source-Root vorhanden ($GOLD_SOURCE_ROOT). NUR nach realem recover (separate Mission)." die "verify-recovered-gold-build-source: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)"
fi 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 exit 0
} }
@ -2457,6 +2676,7 @@ case "$SUBCMD" in
verify-deployed-gold-source) verify_deployed_gold_source "$@";; verify-deployed-gold-source) verify_deployed_gold_source "$@";;
recover-gold-build-source) recover_gold_build_source "$@";; recover-gold-build-source) recover_gold_build_source "$@";;
verify-recovered-gold-build-source) verify_recovered_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 <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-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" *) 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-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"
;; ;;