From 9129e76a6daed51001885ef486451b2fdfec7a36 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Sat, 29 Aug 2026 15:24:28 +0000 Subject: [PATCH] =?UTF-8?q?rq-historical:=20SOT-Discovery=20read-only=20Ka?= =?UTF-8?q?nal=20(build-def,=20build-files,=20host/container-manifest,=20h?= =?UTF-8?q?ost/container-code,=20tree-diff)=20=E2=80=94=20redacted=20Secre?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../control-plane/cp2a2_2b/rq-historical | 233 ++++++++++++------ 1 file changed, 154 insertions(+), 79 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 69273f3..05d352e 100644 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -1,43 +1,36 @@ #!/bin/bash # ============================================================================ # rq-historical — eng begrenzter root-Wrapper fuer Red Queen (rq-host) -# RED-QUEEN-VPS-HOST-ACCESS + PHASE-13.5-READONLY + DEV/TEST/DEPLOY-CHANNEL +# RED-QUEEN-VPS-HOST-ACCESS + READONLY-CODE + DEV/TEST/DEPLOY + SOT-DISCOVERY # | Rain Ocampo (Hermes) | 2026-08-29 # # LEAST-PRIVILEGE-Design: # - rq-host hat KEINE docker-Gruppe, KEINEN Docker-Socket, KEIN sudo ALL, # KEINE Root-Shell. Einziger Root-Kanal: NOPASSWD:/usr/local/bin/rq-historical -# - Dieser Wrapper ist der EINZIGE Kanal zu historical-service. Er laeuft als -# root (via sudoers), erzwingt aber fuer Dev/Test/Deploy strikte Fixierung: -# * feste Staging-Roots, feste Allowlist (KEY->Staging-Datei->Containerpfad) -# * keine freien Pfade, keine ../, kein Symlink-Escape, kein Mount-Arg -# * automatisches Backup + Healthcheck + Hash-Reverify vor/nach Deploy -# * deploy != execution: nie automatischer Backfill/Runner/Watchdog -# - Deploy-Methode: atomic docker cp der validierten Stage-Datei in den -# laufenden Container (Container ist beschreibbar, ReadonlyRootfs=false). -# Rebuild via compose-build NICHT freigegeben (wuerde rq-host Docker-Build -# + Container-Recreation geben -> Punkt 2/10-Verletzung). -# - Test: py_compile der Stage-Kopie in Container-/tmp (keine Mutation des -# laufenden Produktivcodes). +# - Dieser Wrapper ist der EINZIGE Kanal zu historical-service & Host-SoT. +# Keine freien Pfade, kein arbitrary cat/find/grep, kein Shell. # -# SUBKOMMANDOS: -# ps | inspect | logs [--tail N] | run [--start --end --limit-days] (bestehend) -# code | hashes | code-list (bestehend, read-only) -# stage-dir (zeigt Staging-Pfade) -# stage-check (validiert Staging-Kandidat) -# test (py_compile in /tmp, isoliert) -# search-writes (fixe Write-Schreibpfad-Suche) -# healthcheck (/health + /health/ready 200) -# backup (aktuellen Container-Code sichern) -# deploy (Backup->stage-check->test->cp->reverify->health) -# rollback (zurueck zum letzten Backup) +# SoT-DISCOVERY (read-only, NO copy/rebuild/deploy/fix): +# build-def -> zeigt Dockerfile + Compose-Builddefinition (redacted) +# build-files -> Dockerfile + compose + .dockerignore Auflistung +# host-manifest -> SHA256+size aller *.py unter /opt/historical-v2/app +# host-code -> gezielter Code-Read NUR aus host-manifest (kontrolliert) +# container-manifest -> SHA256+size aller *.py unter historical-service:/app/app +# container-code -> gezielter Code-Read NUR aus container-manifest +# tree-diff -> Host vs Container Manifest-Diff +# +# SUBKOMMANDOS (bestehend, unveraendert): +# ps | inspect | logs [--tail N] | run [..] | code | hashes | code-list +# stage-dir | stage-check | test | search-writes | healthcheck +# backup | deploy | rollback # ============================================================================ set -euo pipefail CONTAINER=historical-service +HOST_APP=/opt/historical-v2/app +HOST_DIR=/opt/historical-v2 # --------------------------------------------------------- Allowlist (fest) -# KEY -> Container-Zielpfad (identisch mit 'code'-Read-Allowlist) declare -A TARGET_PATHS=( [runner]="/app/app/run_backfill_year.py" [backfill]="/app/app/backfill.py" @@ -48,40 +41,28 @@ declare -A TARGET_PATHS=( [sanity]="/app/app/trust/sanity.py" [trust_gate]="/app/app/trust/trust_gate.py" ) - -# KEY -> Staging-Dateiname (fest, NUR Dateiname im Staging-Root) declare -A STAGE_FILES=( - [runner]="run_backfill_year.py" - [backfill]="backfill.py" - [pipeline]="pipeline_v2.py" - [repository]="repository.py" - [provenance]="provenance.py" - [trust]="trust.py" - [sanity]="sanity.py" - [trust_gate]="trust_gate.py" + [runner]="run_backfill_year.py" [backfill]="backfill.py" [pipeline]="pipeline_v2.py" + [repository]="repository.py" [provenance]="provenance.py" [trust]="trust.py" + [sanity]="sanity.py" [trust_gate]="trust_gate.py" ) - STAGING_ROOT=/opt/rq-historical-staging BACKUP_ROOT=/opt/rq-historical-backups - -# Relevanter Pfad fuer den 'run'-Subbefehl (Backfill-Entrypoint) SCRIPT=/app/app/run_backfill_year.py -# ------------------------------------------------------------ Validierung die() { echo "ERR: $*" >&2; exit 2; } req_ok() { docker inspect -f '{{.State.Running}}' "$CONTAINER" >/dev/null 2>&1 \ || die "Container '$CONTAINER' nicht vorhanden oder nicht am Laufen" } -key_path() { echo "${TARGET_PATHS[$1]:-}"; } # $1=key -> Containerpfad -stage_file() { echo "$STAGING_ROOT/${STAGE_FILES[$1]:-}"; } # $1=key -> abs Staging-Pfad -key_ok() { # $1=key; exit 2 wenn unbekannt +key_path() { echo "${TARGET_PATHS[$1]:-}"; } +stage_file() { echo "$STAGING_ROOT/${STAGE_FILES[$1]:-}"; } +key_ok() { [ -n "${TARGET_PATHS[$1]:-}" ] && [ -n "${STAGE_FILES[$1]:-}" ] || die "unbekannter KEY '$1' (code-list)" } expect_zero() { [ $# -eq 0 ] || die "unerlaubtes Argument '$*'"; } -# stage-check: FAIL-CLOSED-Validierung eines Staging-Kandidaten -validate_stage() { # $1=key +validate_stage() { local key="$1" file real owner dmode file="$(stage_file "$key")" [ -e "$file" ] || die "Staging-Kandidat fehlt: $file (lege zuerst die Datei ab)" @@ -97,12 +78,17 @@ validate_stage() { # $1=key echo "OK: $key -> $file (regular, no symlink, owner rq-host, in staging)" } -# ---------------------------------------------------- dispatch +# ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ---- +redact_line() { + # Zeilen mit "PASSWORD=" / "SECRET=" / "TOKEN=" : Wert maskieren + echo "$1" | sed -E 's/(PASSWORD|PASSWORDS|SECRET|SECRETS|TOKEN|TOKENS|API_KEY|CLIENT_SECRET|PRIVATE_KEY)(["'"'"']?[[:space:]]*=[[:space:]]*["'"'"']?)[^[:space:]"'"'"']+/\1\2***REDACTED***/Ig' +} + SUBCMD="${1:-}" shift || true case "$SUBCMD" in - # ---------------- bestehend (read-only / run) ---------------- + # ---------------- bestehend ---------------- ps) expect_zero "$@"; exec docker ps ;; inspect) req_ok; expect_zero "$@"; exec docker inspect "$CONTAINER" ;; logs) req_ok @@ -119,16 +105,15 @@ case "$SUBCMD" in declare -a RUN=() while [ $# -gt 0 ]; do case "$1" in - --start) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --start"; RUN+=("--start" "$2"); shift 2 ;; - --end) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --end"; RUN+=("--end" "$2"); shift 2 ;; - --limit-days) [[ "${2:-}" =~ ^[0-9]+$ ]] || die "ungueltiges --limit-days"; RUN+=("--limit-days" "$2"); shift 2 ;; + --start)[[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]||die "ungueltiges --start";RUN+=("--start" "$2");shift 2;; + --end)[[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]||die "ungueltiges --end";RUN+=("--end" "$2");shift 2;; + --limit-days)[[ "${2:-}" =~ ^[0-9]+$ ]]||die "ungueltiges --limit-days";RUN+=("--limit-days" "$2");shift 2;; *) die "unerlaubtes Argument 'run $1'" ;; esac done echo "[rq-historical] Start: docker exec $CONTAINER python $SCRIPT ${RUN[*]}" >&2 exec docker exec "$CONTAINER" python "$SCRIPT" "${RUN[@]}" ;; - # ------------- read-only Code ------------- code) req_ok [ $# -eq 1 ] || die "code braucht genau EINEN Key (code-list)" p="$(key_path "$1")"; [ -n "$p" ] || die "unbekannter Key 'code $1'" @@ -142,15 +127,13 @@ case "$SUBCMD" in echo "Verfuegbare READ-ONLY-Code-Keys (historical-service):" for k in "${!TARGET_PATHS[@]}"; do printf ' %-11s | staging: %-22s -> %s\n' "$k" "${STAGE_FILES[$k]}" "${TARGET_PATHS[$k]}"; done ;; - # ------------- dev/test/deploy-kanal ------------- stage-dir) expect_zero "$@" echo "Staging-Root: $STAGING_ROOT (Owner rq-host, keine globalen Schreibrechte)" for k in runner backfill pipeline repository provenance trust sanity trust_gate; do printf ' %-11s %s\n' "$k" "$(stage_file "$k")" done ;; - stage-check) [ $# -eq 1 ] || die "stage-check braucht genau EINEN Key"; key_ok "$1" - validate_stage "$1" + stage-check) [ $# -eq 1 ] || die "stage-check braucht genau EINEN Key"; key_ok "$1"; validate_stage "$1" ;; test) req_ok; [ $# -eq 1 ] || die "test braucht genau EINEN Key"; key_ok "$1" validate_stage "$1" @@ -161,8 +144,7 @@ case "$SUBCMD" in echo "PASS: py_compile OK ($file) [isoliert in Container-/tmp, Produktivcode unberuehrt]" else echo "FAIL: py_compile fehlgeschlagen ($file)" - docker exec "$CONTAINER" rm -f "$tmp" - exit 2 + docker exec "$CONTAINER" rm -f "$tmp"; exit 2 fi docker exec "$CONTAINER" rm -f "$tmp" ;; @@ -178,53 +160,38 @@ case "$SUBCMD" in echo "HEALTHCHECK OK" ;; backup) req_ok; [ $# -eq 1 ] || die "backup braucht genau EINEN Key"; key_ok "$1" - p="$(key_path "$1")" - ts="$(date +%Y%m%d_%H%M%S)" - bdir="$BACKUP_ROOT/$1/$ts" + p="$(key_path "$1")"; ts="$(date +%Y%m%d_%H%M%S)"; bdir="$BACKUP_ROOT/$1/$ts" mkdir -p "$bdir" docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")" sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256" - echo "BACKUP: $1 -> $bdir" - cat "$bdir/SHA256" + echo "BACKUP: $1 -> $bdir"; cat "$bdir/SHA256" ;; deploy) req_ok; [ $# -eq 1 ] || die "deploy braucht genau EINEN Key"; key_ok "$1" key="$1"; p="$(key_path "$key")"; file="$(stage_file "$key")" - # 1) validate (FAIL CLOSED) validate_stage "$key" - # 2) backup erzwingen (muss gelingen) ts="$(date +%Y%m%d_%H%M%S)"; bdir="$BACKUP_ROOT/$key/$ts" mkdir -p "$bdir" docker exec "$CONTAINER" cat "$p" > "$bdir/$(basename "$p")" || die "Backup fehlgeschlagen -> deploy abgebrochen" sha256sum "$bdir/$(basename "$p")" > "$bdir/SHA256" echo "BACKUP: $bdir" - # 3) test (py_compile isoliert) tmp="/tmp/rqdeploy_${key}_$(date +%s).py" docker cp "$file" "$CONTAINER:$tmp" if ! docker exec "$CONTAINER" python -m py_compile "$tmp" >/dev/null 2>&1; then - docker exec "$CONTAINER" rm -f "$tmp" - die "py_compile FAIL -> deploy abgebrochen (nichts deployt, kein Rollback noetig)" + docker exec "$CONTAINER" rm -f "$tmp"; die "py_compile FAIL -> deploy abgebrochen (nichts deployt)" fi docker exec "$CONTAINER" rm -f "$tmp" - # 4) atomic deploy docker cp "$file" "$CONTAINER:$p" - # 5) hash-reverify sha_stage="$(sha256sum "$file" | awk '{print $1}')" sha_deployed="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')" - echo "SHA stage : $sha_stage" - echo "SHA deployed: $sha_deployed" + echo "SHA stage : $sha_stage"; echo "SHA deployed: $sha_deployed" if [ "$sha_stage" != "$sha_deployed" ]; then - echo "HASH-MISMATCH -> rollback" - docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p" - die "Deploy Hash-Mismatch, zurueckgerollt" + echo "HASH-MISMATCH -> rollback"; docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"; die "Deploy Hash-Mismatch, zurueckgerollt" fi - # 6) healthcheck (nur health/ready, KEIN run) rh="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health", timeout=5).status)' 2>/dev/null || echo N/A)" rr="$(docker exec "$CONTAINER" python -c 'import urllib.request; print(urllib.request.urlopen("http://localhost:8000/health/ready", timeout=5).status)' 2>/dev/null || echo N/A)" echo "Health: /health=$rh /health/ready=$rr" if [ "$rh" != "200" ] || [ "$rr" != "200" ]; then - echo "DEPLOY-HEALTHCHECK FAILED -> rollback" - docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p" - die "Rollback ausgefuehrt" + echo "DEPLOY-HEALTHCHECK FAILED -> rollback"; docker cp "$bdir/$(basename "$p")" "$CONTAINER:$p"; die "Rollback ausgefuehrt" fi echo "DEPLOY OK: $key -> $p (Backup: $bdir). KEIN backfill/run gestartet." ;; @@ -232,8 +199,7 @@ case "$SUBCMD" in p="$(key_path "$1")" bdir="$(ls -1t "$BACKUP_ROOT/$1" 2>/dev/null | head -1)" [ -n "$bdir" ] || die "kein Backup fuer '$1' vorhanden" - bdir="$BACKUP_ROOT/$1/$bdir" - src="$bdir/$(basename "$p")" + bdir="$BACKUP_ROOT/$1/$bdir"; src="$bdir/$(basename "$p")" [ -f "$src" ] || die "Backup-Datei fehlt: $src" sha_bak="$(sha256sum "$src" | awk '{print $1}')" sha_cur="$(docker exec "$CONTAINER" sha256sum "$p" | awk '{print $1}')" @@ -244,6 +210,115 @@ case "$SUBCMD" in [ "$sha_cur" = "$sha_bak" ] || die "ROLLBACK-HASH-MISMATCH" echo "ROLLBACK OK: $key -> $p auf $bdir" ;; - *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback " + + # ============ SOT-DISCOVERY (read-only, NEU) ============ + build-def) expect_zero "$@" + echo "=== Dockerfile ($HOST_DIR/Dockerfile) — redacted ===" + [ -f "$HOST_DIR/Dockerfile" ] || die "Dockerfile nicht gefunden" + while IFS= read -r line; do redact_line "$line"; done < "$HOST_DIR/Dockerfile" + echo "" + echo "=== docker-compose.yml — strukturielle Abschnitte, redacted ===" + cf="" + if [ -f "$HOST_DIR/docker-compose.yml" ]; then cf="$HOST_DIR/docker-compose.yml"; + elif [ -f "$HOST_DIR/compose.yml" ]; then cf="$HOST_DIR/compose.yml"; + elif [ -f "$HOST_DIR/docker-compose.yaml" ]; then cf="$HOST_DIR/docker-compose.yaml"; fi + [ -n "$cf" ] || die "keine compose-Datei gefunden" + echo "(Quelle: $cf)" + while IFS= read -r line; do + # nur relevante strukturelle Schluessel anzeigen; Environment/Secrets nicht roh + case "$line" in + *POSTGRES_PASSWORD*|*PG_PASSWORD*|*PASSWORD*|*SECRET*|*TOKEN*) echo " **** REDACTED (Secret-Zeile) ****" ;; + *) echo " $line" ;; + esac + done < "$cf" + echo "" + echo "=== .dockerignore (falls vorhanden) ===" + [ -f "$HOST_DIR/.dockerignore" ] && cat "$HOST_DIR/.dockerignore" || echo "(kein .dockerignore)" + echo "" + echo "=== Service/Container-Fixierung ===" + echo " compose.project=$(docker inspect "$CONTAINER" --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || echo N/A)" + echo " compose.service=$(docker inspect "$CONTAINER" --format '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo N/A)" + echo " container=$CONTAINER" + ;; + build-files) expect_zero "$@" + echo "=== Build-Dateien unter $HOST_DIR (nicht-verzeichnis, nicht .env/.dump/.sql) ===" + for f in Dockerfile docker-compose.yml compose.yml docker-compose.yaml .dockerignore requirements.txt; do + if [ -e "$HOST_DIR/$f" ]; then printf ' %-22s %s\n' "$f" "EXISTS ($(stat -c '%s' "$HOST_DIR/$f") bytes)"; else printf ' %-22s %s\n' "$f" "(fehlt)"; fi + done + ;; + host-manifest) expect_zero "$@" + echo "=== HOST Manifest /opt/historical-v2/app (*.py, keine Symlinks folgen) ===" + [ -d "$HOST_APP" ] || die "Host-App-Verzeichnis fehlt: $HOST_APP" + # finde reguläre *.py UND Symlinks auf *.py separat + ( + find "$HOST_APP" -type f -name "*.py" -printf '%P %s\n' 2>/dev/null | sort \ + | while read -r rel size; do + sha=$(sha256sum "$HOST_APP/$rel" 2>/dev/null | awk '{print $1}') + printf 'FILE\t%s\t%s\t%s\n' "$rel" "$size" "${sha:-ERR}" + done + find "$HOST_APP" -type l -name "*.py" -printf '%P\n' 2>/dev/null | sort \ + | while read -r rel; do printf 'SYMLINK\t%s\t0\t\n' "$rel"; done + ) + ;; + host-code) [ $# -eq 1 ] || die "host-code braucht genau EINEN relativen Pfad (unter app/)" + rel="$1" + [[ "$rel" == *".."* ]] && die "Traversal nicht erlaubt" + [[ "$rel" == /* ]] && die "relativer Pfad erwartet, nicht absolut" + # realpath zwingend im app-Root + real="$(realpath -m "$HOST_APP/$rel")" + [[ "$real" == "$HOST_APP/"* ]] || die "Pfad ausserhalb app-Root blockiert" + [ -f "$real" ] || die "Datei nicht gefunden oder kein File: $rel" + [ -L "$real" ] && die "Symlink-Read nicht erlaubt" + # nur .py ausgeben + [[ "$rel" == *.py ]] || die "Nur .py-Quelldateien lesbar" + echo "--- $rel ($(stat -c '%s' "$real") bytes) ---" + exec cat "$real" + ;; + container-manifest) req_ok; expect_zero "$@" + echo "=== CONTAINER Manifest historical-service:/app/app (*.py) ===" + docker exec "$CONTAINER" sh -c 'cd /app/app && find . -name "*.py" -type f 2>/dev/null | sed "s#^\./##" | sort | while read f; do size=$(stat -c %s "$f"); sha=$(sha256sum "$f" | cut -d" " -f1); printf "FILE\t%s\t%s\t%s\n" "$f" "$size" "$sha"; done' + docker exec "$CONTAINER" sh -c 'cd /app/app && find . -name "*.py" -type l 2>/dev/null | sed "s#^\./##" | sort | while read f; do printf "SYMLINK\t%s\t0\t\n" "$f"; done' || true + ;; + container-code) req_ok; [ $# -eq 1 ] || die "container-code braucht genau EINEN relativen Pfad (unter /app/app/)" + rel="$1" + [[ "$rel" == *".."* ]] && die "Traversal nicht erlaubt" + [[ "$rel" == /* ]] && die "relativer Pfad erwartet, nicht absolut" + target="/app/app/$rel" + # sicherstellen Ziel existiert als reguläre Datei & nicht symlink im Container + if ! docker exec "$CONTAINER" sh -c "[ -f '$target' ]" 2>/dev/null; then die "Datei nicht gefunden oder kein File im Container: $rel"; fi + if docker exec "$CONTAINER" sh -c "[ -L '$target' ]" 2>/dev/null; then die "Symlink-Read im Container nicht erlaubt"; fi + [[ "$rel" == *.py ]] || die "Nur .py-Quelldateien lesbar" + echo "--- $target ---" + exec docker exec "$CONTAINER" cat "$target" + ;; + tree-diff) req_ok; expect_zero "$@" + echo "=== TREE-DIFF Host(/opt/historical-v2/app) <-> Container(/app/app) ===" + hmap="$(mktemp)"; cmap="$(mktemp)" + # Host: 'relsha' + find "$HOST_APP" -type f -name "*.py" -printf '%P\n' 2>/dev/null | sort | while IFS= read -r rel; do + sha="$(sha256sum "$HOST_APP/$rel" 2>/dev/null | awk '{print $1}')" + printf '%s\t%s\n' "$rel" "$sha" + done > "$hmap" + # Container: 'relsha' + docker exec "$CONTAINER" sh -c 'cd /app/app && find . -type f -name "*.py" -printf "%P\n" 2>/dev/null | sort | while IFS= read -r rel; do sha256sum "$rel" 2>/dev/null | awk -v r="$rel" '\''{print r"\t"$1}'\''; done' > "$cmap" + tmpunion="$(mktemp)" + { cut -f1 "$hmap"; cut -f1 "$cmap"; } | sort -u > "$tmpunion" + echo "HOST_APP=$HOST_APP CONTAINER_APP=/app/app" + while IFS= read -r rel; do + hs="$(awk -F'\t' -v r="$rel" '$1==r{print $2}' "$hmap")" + cs="$(awk -F'\t' -v r="$rel" '$1==r{print $2}' "$cmap")" + if [ -n "$hs" ] && [ -n "$cs" ]; then + if [ "$hs" = "$cs" ]; then status="MATCH"; else status="DIVERGED"; fi + printf '%-18s %s [h:%s c:%s]\n' "$status" "$rel" "${hs:0:12}" "${cs:0:12}" + elif [ -n "$hs" ]; then + printf '%-18s %s\n' "HOST_ONLY" "$rel" + else + printf '%-18s %s\n' "MISSING_ON_HOST" "$rel" + fi + done < "$tmpunion" + rm -f "$hmap" "$cmap" "$tmpunion" + ;; + + *) 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" ;; esac