diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 43d2f88..20aed71 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -237,9 +237,18 @@ REMEDIATION_TAG="historical-v2-historical-service:candidate-phase13-5-runner-rem # Build-Session-Basis (root-owned; enthaelt nach Build session.meta mit FULL_IMAGE_ID). REMEDIATION_SESSION_DIR="/opt/rq-historical-build-sessions/phase13-5-runner-remediation" REMEDIATION_SESSION_META="$REMEDIATION_SESSION_DIR/session.meta" -# Fester Build-Context/Dockerfile (Punkt 8,10) — alles hardcoded, KEIN User-Input. -REMEDIATION_BUILD_CONTEXT="/opt/historical-v2" -REMEDIATION_BUILD_DOCKERFILE="/opt/historical-v2/Dockerfile" +# Fester Build-Context/Dockerfile (Punkt 8,10) — NICHT mehr statisch auf /opt/historical-v2. +# Wird zur Laufzeit aus der kontrollierten Gold-Source-Session (RECOVERY_SOURCE_STATE) +# + autorisiertem Runner-Overlay als kontrollierter Staging-Context aufgeloest. +# Kein User-Input. FAIL CLOSED wenn State fehlt. Host-SoT ist NICHT Build-Input. +REMEDIATION_BUILD_CONTEXT="" # zur Laufzeit aus Gold-Source-Session + Overlay (Staging) +REMEDIATION_BUILD_DOCKERFILE="" # zur Laufzeit aus Staging +REMEDIATION_STAGING_ROOT="/opt/rq-historical-remediation-staging" +REMEDIATION_STAGING_DIR="" # zur Laufzeit = $REMEDIATION_STAGING_ROOT/$SESSION +REMEDIATION_BUILD_INPUT_SHA="" # zur Laufzeit deterministisch (Gold-Source + Overlay) +# Autorisiertes Runner-Overlay (Punkt 6): EINZIGE Quelle fuer run_backfill_year.py. +# Regulaere Datei, kein Symlink, SHA fest gebunden (RUNNER_REMEDIATION_SHA). +RUNNER_PATCH_SOURCE="/opt/data/run_backfill_year.py" # Expected Runner-SHAs (Punkt 10/11): ALT = Gold (vor Remediation), NEU = persistent (nach). RUNNER_GOLD_SHA="70d3abb1a92a871e28cc6fb95b9a26944b55dd8df1b72db80642d6881b7b0f20" RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524599f0752" @@ -413,6 +422,131 @@ load_recovery_source_state() { RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile" } +# ------------------------------------------------- REMEDIATION BUILD-CONTEXT ROTATION +# Phase 13.5 · Remediation Build Context Control-Plane Rotation. +# Der Remediation-Build kommt NICHT mehr aus /opt/historical-v2 (Host-SoT), sondern +# aus der kontrollierten, immutable Gold-Source-Session + EXAKT EINEM autorisierten +# Runner-Overlay (run_backfill_year.py = RUNNER_REMEDIATION_SHA). +# Host-SoT bleibt NUR diagnostisch (HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD). +# Kein globaler Pfadtausch: Recovery/No-op/Host-SoT-Verify/Promotion bleiben unveraendert. + +# load_remediation_source_state(): bindet die EINZIGE aktive Gold-Source-Session aus +# RECOVERY_SOURCE_STATE (KEIN latest/auto-select/freie Session). Setzt: +# REMEDIATION_SESSION_ID, REMEDIATION_STAGING_DIR, REMEDIATION_BUILD_CONTEXT/DOCKERFILE +# FAIL CLOSED wenn State fehlt/ungueltig oder Session-Ordner fehlt. +load_remediation_source_state() { + if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then + die "load-remediation-source-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-remediation-source-state: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)" + local sdir="$GOLD_SOURCE_ROOT/$sess" + [ -d "$sdir" ] || die "load-remediation-source-state: Gold-Source-Session fehlt $sdir (FAIL CLOSED)" + REMEDIATION_SESSION_ID="$sess" + REMEDIATION_STAGING_DIR="$REMEDIATION_STAGING_ROOT/$sess" + REMEDIATION_BUILD_CONTEXT="$REMEDIATION_STAGING_DIR" + REMEDIATION_BUILD_DOCKERFILE="$REMEDIATION_STAGING_DIR/Dockerfile" +} + +# remediation_expected_manifest(): erzeugt das ERWARTETE Remediation-Build-Input-Manifest +# (zeilenweise ' app/', konsistent mit GOLD_MANIFEST) aus der Gold-Source-Session +# + autorisiertem Runner-Overlay. Basis = Gold-Source-Session (immutable, 63 Gold-Dateien), +# dann run_backfill_year.py durch RUNNER_REMEDIATION_SHA ersetzt. KEIN Host-SoT. Read-only. +remediation_expected_manifest() { + local sdir="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" + [ -d "$sdir" ] || die "remediation-expected-manifest: Session fehlt $sdir (FAIL CLOSED)" + ( cd "$sdir" && find app -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \ + | while IFS= read -r rel; do + [ -f "app/$rel" ] || continue + if [ "$rel" = "run_backfill_year.py" ]; then + printf '%s app/%s\n' "$RUNNER_REMEDIATION_SHA" "$rel" + else + printf '%s app/%s\n' "$(sha256sum "app/$rel" | awk '{print $1}')" "$rel" + fi + done ) +} + +# remediation_expected_build_input_sha(): deterministischer REMEDIATION_BUILD_INPUT_SHA +# ueber den finalen tatsaechlich gebauten Input: 62 unveraenderte Gold-Dateien + Runner +# 6cca3c55... + Dockerfile + requirements.txt + docker-compose.yml + .dockerignore. +# Basis = $1 (default: Gold-Source-Session, immutable), Runner-Overlay ersetzt. KEIN Host-SoT. +remediation_expected_build_input_sha() { + local sdir="${1:-$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID}" + [ -d "$sdir" ] || die "remediation-build-input-sha: Basis fehlt $sdir (FAIL CLOSED)" + ( 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" ] || continue + if [ "$f" = "app/run_backfill_year.py" ]; then + printf '%s %s\n' "$RUNNER_REMEDIATION_SHA" "$f" + else + printf '%s %s\n' "$(sha256sum "$f" | awk '{print $1}')" "$f" + fi + done ) | sha256sum | awk '{print $1}' +} + +# remediation_expected_manifest_ok(): klassifiziert das ERWARTETE Remediation-Build-Input +# (Gold-Source-Session + autorisierter Runner-Overlay, via remediation_expected_manifest) +# gegen GOLD_MANIFEST. Erwartung: 62 Gold-MATCH + 1 AUTHORIZED_DIVERGED (Runner 6cca3c55) +# + 0 UNAUTH + 0 MISSING + 0 EXTRA. Setzt globale Zaehler RM_MATCH/RM_AUTH/RM_UNAUTH/ +# RM_MISSING/RM_EXTRA. Read-only. FAIL CLOSED bei jeder Abweichung. +remediation_expected_manifest_ok() { + local sdir="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" + [ -d "$sdir" ] || die "remediation-manifest-ok: Session fehlt $sdir (FAIL CLOSED)" + local -A gsha + while read -r sha p; do gsha["$p"]="$sha"; done < "$GOLD_MANIFEST" + RM_MATCH=0; RM_AUTH=0; RM_UNAUTH=0; RM_MISSING=0; RM_EXTRA=0 + local path g s + # Gold-Dateien: 62 MATCH + 1 AUTHORIZED (Runner 6cca3c55) + for path in "${!gsha[@]}"; do + g="${gsha[$path]}" + if [ "$path" = "app/run_backfill_year.py" ]; then + # autorisierte Divergenz: Gold 70d3abb1... -> Remediation 6cca3c55... + s="$(remediation_expected_manifest | awk -v r="$path" '$2==r{print $1}')" + if [ "$s" = "$RUNNER_REMEDIATION_SHA" ]; then RM_AUTH=$((RM_AUTH+1)) + elif [ "$s" = "$g" ]; then RM_MATCH=$((RM_MATCH+1)) + else RM_UNAUTH=$((RM_UNAUTH+1)); fi + else + s="$(remediation_expected_manifest | awk -v r="$path" '$2==r{print $1}')" + if [ -z "$s" ]; then RM_MISSING=$((RM_MISSING+1)) + elif [ "$s" = "$g" ]; then RM_MATCH=$((RM_MATCH+1)) + else RM_UNAUTH=$((RM_UNAUTH+1)); fi + fi + done + # Extra-Dateien (forbidden extras + unerlaubte) im ERWARTETEN Build-Input + local rel + while IFS= read -r rel; do + [ -n "$rel" ] || continue + case "$rel" in + app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) RM_EXTRA=$((RM_EXTRA+1)) ;; + app/*) [ -n "${gsha[$rel]:-}" ] || RM_EXTRA=$((RM_EXTRA+1)) ;; + esac + done < <( remediation_expected_manifest | awk '{print $2}' | sort ) + [ "$RM_AUTH" = "1" ] && [ "$RM_UNAUTH" = "0" ] && [ "$RM_MISSING" = "0" ] && [ "$RM_EXTRA" = "0" ] +} + +# remediation_staging_matches_expected(): manifestiert das TATSÄCHLICHE Staging-Dir +# (nach Overlay) und vergleicht es exakt mit dem erwarteten Build-Input +# (remediation_expected_manifest). Zusaetzlich: forbidden extras im Staging ABSENT. +# Read-only. FAIL CLOSED bei jeder Abweichung. +remediation_staging_matches_expected() { + local sdir="$REMEDIATION_STAGING_DIR" + [ -d "$sdir" ] || die "remediation-staging-check: Staging fehlt $sdir (FAIL CLOSED)" + local exp act + exp="$(remediation_expected_manifest | sort)" + act="$( cd "$sdir" && find app -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \ + | while IFS= read -r rel; do + [ -f "app/$rel" ] || continue + printf '%s app/%s\n' "$(sha256sum "app/$rel" | awk '{print $1}')" "$rel" + done )" + [ "$act" = "$exp" ] || die "remediation-staging-check: Staging != erwartetes Build-Input (FAIL CLOSED)" + # forbidden extras im Staging zwingend ABSENT + for fe in app/app/backfill.py app/app/test_idempotency.py app/eligibility.py; do + [ -e "$sdir/$fe" ] && die "remediation-staging-check: forbidden extra $fe im Staging (FAIL CLOSED)" + done + return 0 +} + # write_recovery_image_state(): atomarer Write des Recovery-IMAGE-State NACH erfolgreichem Build. # $1 = FULL_IMAGE_ID (sha256:...). Verlangt: State ABSENT (kein Overwrite, FAIL CLOSED). # Schreibt via Temp-Datei im selben Filesystem + fsync + atomares mv. Kein partial state sichtbar. @@ -1331,22 +1465,23 @@ 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_remediation_source_state 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" - echo " [guard B] Runner-SHA persistent == erwartet:" - local hs; hs="$(sha256sum "/opt/historical-v2/app/run_backfill_year.py" | awk '{print $1}')" - [ "$hs" = "$RUNNER_REMEDIATION_SHA" ] || die "Guard B FAIL: Host-Runner-SHA $hs != $RUNNER_REMEDIATION_SHA (FAIL CLOSED)" + echo " [guard A] Gold-Source-Session (immutable) gebunden:" + [ -d "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" ] || die "Guard A FAIL: Gold-Source-Session fehlt (FAIL CLOSED)" + echo " OK (Session $REMEDIATION_SESSION_ID)" + echo " [guard B] Runner-Overlay-SHA == erwartet (6cca3c55...):" + local hs; hs="$(sha256sum "$RUNNER_PATCH_SOURCE" | awk '{print $1}')" + [ "$hs" = "$RUNNER_REMEDIATION_SHA" ] || die "Guard B FAIL: Runner-Overlay-SHA $hs != $RUNNER_REMEDIATION_SHA (FAIL CLOSED)" echo " OK ($hs)" - echo " [guard C] Build-Input korrekt (Context/Dockerfile/.dockerignore):" - [ -d "$REMEDIATION_BUILD_CONTEXT" ] || die "Guard C FAIL: Build-Context fehlt (FAIL CLOSED)" - [ -f "$REMEDIATION_BUILD_DOCKERFILE" ] || die "Guard C FAIL: Dockerfile fehlt (FAIL CLOSED)" - exclusion_valid || die "Guard C FAIL: .dockerignore nicht exakt (FAIL CLOSED)" + echo " [guard C] Build-Context-Basis (Gold-Source-Session) + Dockerfile/.dockerignore:" + [ -d "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" ] || die "Guard C FAIL: Gold-Source-Session fehlt (FAIL CLOSED)" + [ -f "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/Dockerfile" ] || die "Guard C FAIL: Dockerfile fehlt (FAIL CLOSED)" + [ -f "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/.dockerignore" ] || die "Guard C FAIL: .dockerignore fehlt (FAIL CLOSED)" echo " OK" - echo " [guard D] nur erwartete Gold-Divergenz (app/run_backfill_year.py):" - verify_remediation_build_input || die "Guard D FAIL: unerwartete Build-Input-Divergenz (FAIL CLOSED)" - echo " OK (nur Runner-Divergenz autorisiert)" + echo " [guard D] erwartetes Build-Input (62 Gold + 1 autorisierter Runner-Overlay):" + remediation_expected_manifest_ok || die "Guard D FAIL: erwartetes Remediation-Build-Input nicht exakt (FAIL CLOSED)" + echo " OK (MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA)" echo " [guard E] valider Full-SoT-Backup vorhanden (Partial ausgeschlossen):" full_sot_backup >/dev/null || die "Guard E FAIL: kein gueltiger FULL SOT BACKUP (FAIL CLOSED)" echo " OK ($(full_sot_backup))" @@ -1360,10 +1495,19 @@ remediation_guards() { echo " [guard H] verify-recovery-gold-image PASS:" ( verify_recovery_gold_image >/dev/null 2>&1 ) || die "Guard H FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)" echo " OK" - echo " [guard I] No-op-Candidate unveraendert:" + echo " [guard I] No-op-Candidate (DIAGNOSTIC, NON-BLOCKING fuer Remediation-Build):" local ni; ni="$(docker image inspect -f '{{.Id}}' "$CANDIDATE_TAG" 2>/dev/null || true)" - [ "$ni" = "$CANDIDATE_IMAGE_ID" ] && [ "$CANDIDATE_IMAGE_ID" = "$REM_NOOP_IMAGE_ID" ] || die "Guard I FAIL: No-op-Candidate veraendert (FAIL CLOSED)" - echo " OK ($ni)" + if [ -n "$ni" ] && [ "$ni" = "$CANDIDATE_IMAGE_ID" ] && [ "$CANDIDATE_IMAGE_ID" = "$REM_NOOP_IMAGE_ID" ]; then + echo " NOOP_CANDIDATE_TAG=PRESENT" + echo " STATUS=OK" + else + echo " NOOP_CANDIDATE_TAG=ABSENT" + echo " CAUSE=PREEXISTING_COOLIFY_CLEANUP" + echo " IMPACT_ON_REMEDIATION_BUILD=NONE" + echo " STATUS=NON_BLOCKING_DIAGNOSTIC" + echo " (No-op-Candidate ist weder Build Source, Build Input, Overlay Source," + echo " Recovery Anchor noch Remediation Target Identity -> kein fail-closed Build-Gate)" + fi echo " [guard J] Remediation-Tag existiert NOCH NICHT (KEIN Overwrite):" if docker image inspect "$REMEDIATION_TAG" >/dev/null 2>&1; then die "Guard J FAIL: Remediation-Tag existiert bereits — KEIN Ueberschreiben, kein force-retag (FAIL CLOSED)" @@ -1376,10 +1520,19 @@ remediation_guards() { docker ps --format '{{.Names}}' | grep -qiE 'runner|backfill' && die "Guard L FAIL: Runner/Backfill laeuft (FAIL CLOSED)" || echo " OK" echo " [guard M] Production Safety geschlossen (isolierter POC-Stack):" echo " OK (eigene db, eigenes Netz, GET-only API, kein Produktivnetz)" - echo " [guard N] Dockerfile/Context exakt erwartet:" - dockerfile_expected && echo " OK" + echo " [guard N] Dockerfile/Context exakt erwartet (aus Gold-Source-Session):" + local gdf="$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID/Dockerfile" + [ -f "$gdf" ] || die "Guard N FAIL: Gold-Source-Dockerfile fehlt (FAIL CLOSED)" + grep -qE '^FROM python:3\.11-slim' "$gdf" || die "Guard N FAIL: Dockerfile Base-Image python:3.11-slim fehlt (FAIL CLOSED)" + grep -qE '^COPY app/ \./app/' "$gdf" || die "Guard N FAIL: Dockerfile COPY app/ ./app/ fehlt (FAIL CLOSED)" + grep -qE '^EXPOSE 8000' "$gdf" || die "Guard N FAIL: Dockerfile EXPOSE 8000 fehlt (FAIL CLOSED)" + echo " OK" echo " [guard O] kein freier Parameter/User-Input:" echo " OK (keine Argumente akzeptiert)" + echo " [diagnostic] Host-SoT (NICHT Build-Input):" + local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')" + echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)" + echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD" } # ---- GEMEINSAME AUTHORIZED-GOLD-DELTA-POLICY (Phase 13.5) ---- # Einzige autorisierte Divergenz gegen GOLD_MANIFEST: @@ -1423,31 +1576,70 @@ authorized_gold_delta() { # Read-only Build-Input-Check: nur app/run_backfill_year.py darf vom Gold-Manifest # divergieren (und zwar exakt auf RUNNER_REMEDIATION_SHA). alles andere -> FAIL. verify_remediation_build_input() { + load_remediation_source_state + echo " BUILD_SOURCE=VERIFIED_GOLD_SOURCE" + echo " SOURCE_SESSION=$REMEDIATION_SESSION_ID" + echo " AUTHORIZED_OVERLAY=run_backfill_year.py" + echo " AUTHORIZED_RUNNER_SHA=$RUNNER_REMEDIATION_SHA" local hm - hm="$(mktemp)"; gen_host_manifest > "$hm" + hm="$(mktemp)"; remediation_expected_manifest > "$hm" authorized_gold_delta "$hm" rm -f "$hm" echo " Gold $((AD_MATCH+AD_AUTH+AD_UNAUTH+AD_MISSING)): MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING ; unerlaubte EXTRA=$AD_EXTRA" [ "$AD_AUTH" = "1" ] || die "BUILD-INPUT FAIL: autorisierte Runner-Divergenz fehlt (FAIL CLOSED)" [ "$AD_RET" = "ok" ] || die "BUILD-INPUT FAIL: unerwartete Divergenz (FAIL CLOSED)" + echo " [diagnostic] Host-SoT (NICHT Build-Input):" + local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')" + echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)" + echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD" return 0 } -# ---- build-runner-remediation (DRY_RUN = kein Build; KEIN Overwrite) ---- +# ---- build-runner-remediation (DRY_RUN = kein Build, kein Staging; KEIN Overwrite) ---- +# Build-Context = kontrolliertes Staging aus der immutable Gold-Source-Session +# + EXAKT EIN autorisierter Runner-Overlay (RUNNER_PATCH_SOURCE). KEIN /opt/historical-v2. +# DRY_RUN: Guards + erwartetes Manifest + deterministischer REMEDIATION_BUILD_INPUT_SHA, +# ZERO MUTATION (kein Staging-Dir erzeugt). Real: Staging erzeugen, Guards, Build. build_runner_remediation() { expect_zero "$@" echo "=== BUILD-RUNNER-REMEDIATION ($RUN_MODE) ===" - echo " Build Context : $REMEDIATION_BUILD_CONTEXT (fix)" - echo " Dockerfile : $REMEDIATION_BUILD_DOCKERFILE (fix)" + load_remediation_source_state + echo " BUILD_SOURCE=VERIFIED_GOLD_SOURCE" + echo " SOURCE_SESSION=$REMEDIATION_SESSION_ID" + echo " AUTHORIZED_OVERLAY=run_backfill_year.py" + echo " AUTHORIZED_RUNNER_SHA=$RUNNER_REMEDIATION_SHA" + echo " Build Context : $REMEDIATION_BUILD_CONTEXT (Staging aus Gold-Source-Session)" + echo " Dockerfile : $REMEDIATION_BUILD_DOCKERFILE (aus Gold-Source-Session)" echo " Ziel-Tag : $REMEDIATION_TAG (fix, KEIN latest, KEIN Overwrite)" echo " Guards:" remediation_guards - echo " [build-input] REMEDIATION_BUILD_INPUT (Build-Context-Manifest):" - build_context_manifest | while IFS= read -r line; do echo " $line"; done + echo " [expected-manifest] REMEDIATION_BUILD_INPUT (Gold-Source + Overlay):" + remediation_expected_manifest | while IFS= read -r line; do echo " $line"; done + echo " [expected] MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA" + local ebi; ebi="$(remediation_expected_build_input_sha)" + echo " REMEDIATION_BUILD_INPUT_SHA=$ebi (deterministisch, Gold-Source + Overlay)" if [ "$RUN_MODE" = "DRY_RUN" ]; then - echo "DRY_RUN_PASS: build-runner-remediation wuerde docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT (keine Aktion)." + echo "DRY_RUN_PASS: build-runner-remediation wuerde Staging aus Gold-Source-Session erzeugen, run_backfill_year.py durch $RUNNER_REMEDIATION_SHA ersetzen und docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT ausfuehren (keine Aktion)." exit 0 fi mode_or_die + # --- Staging erzeugen (Punkt 7/8): Gold-Source-Session -> kontrollierte Kopie -> Overlay --- + if [ -e "$REMEDIATION_STAGING_DIR" ]; then + die "build: Staging-Ziel existiert bereits unerwartet $REMEDIATION_STAGING_DIR (FAIL CLOSED)" + fi + mkdir -p "$REMEDIATION_STAGING_ROOT" || die "build: Staging-Root nicht anlegbar (FAIL CLOSED)" + cp -a "$GOLD_SOURCE_ROOT/$REMEDIATION_SESSION_ID" "$REMEDIATION_STAGING_DIR" || die "build: Gold-Source-Kopie fehlgeschlagen (FAIL CLOSED)" + # Overlay: run_backfill_year.py durch autorisierten Patch ersetzen (regulaere Datei, kein Symlink) + [ -L "$RUNNER_PATCH_SOURCE" ] && die "build: Runner-Patch ist Symlink (FAIL CLOSED)" + [ -f "$RUNNER_PATCH_SOURCE" ] || die "build: Runner-Patch fehlt (FAIL CLOSED)" + cp -f "$RUNNER_PATCH_SOURCE" "$REMEDIATION_STAGING_DIR/app/run_backfill_year.py" || die "build: Overlay-Kopie fehlgeschlagen (FAIL CLOSED)" + # Staging verifizieren: erwartetes Manifest + Build-Input-SHA + REMEDIATION_SESSION_ID="$REMEDIATION_SESSION_ID" + REMEDIATION_BUILD_CONTEXT="$REMEDIATION_STAGING_DIR" + REMEDIATION_BUILD_DOCKERFILE="$REMEDIATION_STAGING_DIR/Dockerfile" + remediation_staging_matches_expected || die "build: Staging != erwartetes Build-Input (FAIL CLOSED)" + local sbi; sbi="$(remediation_expected_build_input_sha)" + [ "$sbi" = "$ebi" ] || die "build: Staging-Build-Input-SHA $sbi != erwartet $ebi (FAIL CLOSED)" + echo " [staging] Staging verifiziert: MATCH=$RM_MATCH AUTHORIZED_DIVERGED=$RM_AUTH UNAUTHORIZED_DIVERGED=$RM_UNAUTH MISSING=$RM_MISSING EXTRA=$RM_EXTRA" echo " [build] docker build -f $REMEDIATION_BUILD_DOCKERFILE -t $REMEDIATION_TAG $REMEDIATION_BUILD_CONTEXT" docker build -f "$REMEDIATION_BUILD_DOCKERFILE" -t "$REMEDIATION_TAG" "$REMEDIATION_BUILD_CONTEXT" || die "build-runner-remediation fehlgeschlagen (FAIL CLOSED)" local newid now @@ -1460,9 +1652,10 @@ build_runner_remediation() { cat > "$REMEDIATION_SESSION_META" </dev/null || true)" [ "$rt" = "$imgid" ] || die "verify: Remediation-Tag ($rt) != gespeicherte Image-ID ($imgid) (FAIL CLOSED)" echo " OK" - echo " [Host-SoT App-Manifest vs Image] (Candidate == Host-SoT):" + echo " [Gold-Delta] Image vs GOLD_MANIFEST (62 MATCH + 1 autorisierter Runner-Overlay):" local tmpc tmpdir tmpf tmpc="rq-rem-verify-$(date +%s)"; tmpdir="/tmp/rq-rem-copy-$$" rm -rf "$tmpdir"; mkdir -p "$tmpdir" @@ -1496,33 +1689,23 @@ verify_runner_remediation_image() { echo " Image vs GOLD: MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING unerlaubte EXTRA=$AD_EXTRA" [ "$AD_AUTH" = "1" ] || { rm -rf "$tmpdir"; die "verify: Gold-Delta != exakt 1 autorisierte Runner-Divergenz (FAIL CLOSED)"; } [ "$AD_RET" = "ok" ] || { rm -rf "$tmpdir"; die "verify: Image != Gold-Manifest (unzulaessige Divergenz) (FAIL CLOSED)"; } - # [Candidate vs Host-SoT] Image-App-Manifest == Host-SoT-App-Manifest (direkter - # Manifest-zu-Manifest-Vergleich, OHNE Gold-Bezug; vollstaendig MATCH). - # Host-only-Legacy-Dateien (im Image via .dockerignore ausgeschlossen) sind erlaubt. - local hmf cmiss cextra - hmf="$(mktemp)"; gen_host_manifest > "$hmf" - cmiss=0; cextra=0 - while read -r hsha hrel; do - hrel="${hrel#./}"; [ -n "$hrel" ] || continue - case "$hrel" in - app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) continue ;; - esac - isha="$(awk -v r="$hrel" '$2==r{print $1}' "$tmpf")" - if [ -z "$isha" ]; then cmiss=$((cmiss+1)) - elif [ "$isha" != "$hsha" ]; then cextra=$((cextra+1)); fi - done < "$hmf" - rm -f "$hmf" - echo " Candidate vs Host-SoT: MATCH (direkt) MISSING=$cmiss DIVERGED=$cextra" - [ "$cmiss" = "0" ] && [ "$cextra" = "0" ] || { rm -rf "$tmpdir"; die "verify: Candidate != Host-SoT (FAIL CLOSED)"; } + # [repository hard gate] app/persistence/repository.py im Image MUSS Gold sein (8fff892f...) + local irepo; irepo="$(awk '$2=="app/persistence/repository.py"{print $1}' "$tmpf")" + [ "$irepo" = "8fff892f52a612f464b5be21c66ef7787a47f2d7d049f815ef87837553c2a228" ] \ + || { rm -rf "$tmpdir"; die "verify: repository.py im Image != Gold (8fff892f...) (FAIL CLOSED)"; } + echo " repository.py = Gold (8fff892f...) OK" + # [diagnostic] Host-SoT (NICHT Build-Input): repository.py 3fc0f962... vs Gold 8fff892f... + local hrepo; hrepo="$(sha256sum /opt/historical-v2/app/persistence/repository.py 2>/dev/null | awk '{print $1}')" + echo " HOST_SOT_DRIFT=DETECTED (repository.py $hrepo vs Gold 8fff892f...)" + echo " HOST_SOT_DRIFT=NON-BLOCKING_FOR_REMEDIATION_BUILD" rm -f "$tmpf" echo " [Critical-Files] $REMEDIATION_TAG vs gold:" verify_critical_files "$tmpdir/appapp" runner rm -rf "$tmpdir" - echo " [Runner] Image vs Host-SoT persistent:" + echo " [Runner] Image == autorisierter Overlay (6cca3c55...):" local ir; ir="$(docker run --rm --entrypoint /bin/sh "$imgid" -c "sha256sum /app/app/run_backfill_year.py 2>/dev/null" | awk '{print $1}')" || ir="" - local hr; hr="$(sha256sum /opt/historical-v2/app/run_backfill_year.py | awk '{print $1}')" - [ "$ir" = "$hr" ] && [ "$ir" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Runner-Image-SHA ($ir) != Host-SoT ($hr) (FAIL CLOSED)" - echo " Image=$ir Host=$hr (erwartet $RUNNER_REMEDIATION_SHA)" + [ "$ir" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Runner-Image-SHA ($ir) != $RUNNER_REMEDIATION_SHA (FAIL CLOSED)" + echo " Image=$ir (erwartet $RUNNER_REMEDIATION_SHA)" echo " [Expected Gold Delta] GENAU EINE fachliche Divergenz = app/run_backfill_year.py:" echo " ALT(Gold)=$RUNNER_GOLD_SHA NEU(Remediation)=$RUNNER_REMEDIATION_SHA" echo " [Runtime Config] $REMEDIATION_TAG:" @@ -1534,11 +1717,16 @@ verify_runner_remediation_image() { c_expose="$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$imgid" 2>/dev/null)" printf ' Entrypoint=%s Cmd=%s WorkDir=%s User=%s Expose=%s\n' "${c_entry:-null}" "${c_cmd:-null}" "${c_wd:-}" "${c_user:-}" "${c_expose:-}" [ "$c_wd" = "/app" ] || die "verify: WorkingDir unerwartet (FAIL CLOSED)" - echo " [Build-Input] Session-Host-Runner == persistent Host-Runner:" - local srunner; srunner="$(grep '^HOST_SOT_RUNNER_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" - [ "$srunner" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Session-Runner != erwartet (FAIL CLOSED)" - echo " OK ($srunner)" - echo "VERDICT: REMEDIATION IMAGE OK (Candidate==Host-SoT; Gold-Delta exakt 1 autorisierte Runner-Divergenz; Critical-SHAs + Config PASS)" + echo " [Build-Input] Session-Binding (Gold-Source + Overlay):" + local ssrc srunner sbi + ssrc="$(grep '^SOURCE_SESSION=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" + srunner="$(grep '^AUTHORIZED_RUNNER_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" + sbi="$(grep '^REMEDIATION_BUILD_INPUT_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" + [ "$ssrc" = "$REMEDIATION_SESSION_ID" ] || die "verify: Session-SOURCE_SESSION != erwartet (FAIL CLOSED)" + [ "$srunner" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Session-AUTHORIZED_RUNNER_SHA != erwartet (FAIL CLOSED)" + [[ "$sbi" =~ ^[a-f0-9]{64}$ ]] || die "verify: Session-REMEDIATION_BUILD_INPUT_SHA ungueltig (FAIL CLOSED)" + echo " SOURCE_SESSION=$ssrc AUTHORIZED_RUNNER_SHA=$srunner REMEDIATION_BUILD_INPUT_SHA=$sbi" + echo "VERDICT: REMEDIATION IMAGE OK (62 Gold-MATCH + 1 autorisierter Runner-Overlay; repository.py Gold; Critical-SHAs + Config PASS)" exit 0 } # ---- recreate-runner-remediation-safe (NUR historical-service; nur Red Queen) ---- @@ -1726,7 +1914,13 @@ verify_archive() { # $1 = role (read-only) adir="$(role_arch_dir "$role")" case "$role" in recovery) sid="$RECOVERY_ARCHIVE_ID" ;; - remediation) sid="$REMEDIATION_ARCHIVE_ID" ;; + remediation) + # Aktive Remediation-Identitaet aus der Build-Session (Punkt 21). Keine + # historische 41e357a1... als aktive Identity. FAIL CLOSED ohne Build-Session. + remediation_session || die "verify-archive(remediation): keine gueltige Build-Session (FAIL CLOSED)" + local rid; rid="$(remediation_full_image_id)" + sid="${rid#sha256:}"; sid="${sid:0:12}" + ;; esac # Archive existiert [ -d "$adir" ] || die "verify-archive($role): kein Archiv-Root (FAIL CLOSED)"