Files
awoooi/scripts/backup/host110-backup-runtime-executor.sh
2026-07-19 01:08:45 +08:00

979 lines
34 KiB
Bash
Executable File

#!/usr/bin/env bash
# Fixed Host110 executor for Windows99 Agent99 backup-runtime deployment.
set -euo pipefail
umask 077
readonly EXPECTED_HOST_IP="192.168.0.110"
readonly EXPECTED_USER="wooo"
readonly DEST_ROOT="/backup/scripts"
readonly EXPORTER_ROOT="/home/wooo/scripts"
readonly EXPORTER_FILE="backup-health-textfile-exporter.py"
readonly STATUS_ROOT="/backup/status"
readonly STAGE_ROOT="/backup/.agent99-backup-runtime-stage"
readonly ROLLBACK_ROOT="/backup/.agent99-backup-runtime-rollback"
readonly LOCK_PATH="/tmp/agent99-host110-backup-runtime.lock"
readonly -a RUNTIME_FILES=(
common.sh
backup-all.sh
backup-host188-products.sh
verify-host188-products-backup.sh
verify-host188-products-archive.py
backup-gitea.sh
gitea-full-backup-restore-drill.sh
backup-configs.sh
backup-awoooi.sh
backup-awoooi-frequent.sh
backup-clawbot.sh
backup-sentry.sh
check-backup-integrity.sh
sync-offsite-backups.sh
backup-offsite-readiness-gate.sh
verify-offsite-full-sync.sh
enforce-latest-only-retention.sh
)
readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")
MODE=""
SOURCE_REVISION=""
RUN_ID=""
SOURCE_STAGE=""
SOURCE_HEAD=""
EXECUTOR_DIGEST=""
VERIFIER_DIGEST=""
STAGE_DIR=""
ROLLBACK_DIR=""
RECEIPT_PATH=""
TERMINAL_RECEIPT_PATH=""
APPLY_STARTED=0
APPLY_COMPLETE=0
COMMIT_CRITICAL=0
DEFERRED_SIGNAL=""
DEFERRED_SIGNAL_EXIT=0
ROLLBACK_ATTEMPTED=0
ROLLBACK_PERFORMED=0
ROLLBACK_VERIFIED=0
RUN_TEMP_RESIDUE_VERIFIED=0
FAULT_INJECTION_ENABLED=0
declare -A FILE_EXISTED=()
declare -A PREVIOUS_DIGEST=()
declare -A PREVIOUS_METADATA=()
declare -A EXPECTED_DIGEST=()
declare -A RUN_OWNED_TEMP_PATHS=()
usage() {
printf '%s\n' \
"Usage: $0 --mode check|apply|verify --source-revision SHA --run-id ID --source-stage PATH" \
"The executor is fixed to host 192.168.0.110, an Agent99 run-bound source stage," \
"and the 18-file transaction: 17 /backup/scripts files plus the backup health exporter."
}
fail() {
printf 'HOST110_BACKUP_RUNTIME_OK=0\nERROR=%s\n' "$1" >&2
exit 64
}
while [ "$#" -gt 0 ]; do
case "$1" in
--mode)
shift
MODE="${1:-}"
;;
--source-revision)
shift
SOURCE_REVISION="${1:-}"
;;
--run-id)
shift
RUN_ID="${1:-}"
;;
--source-stage)
shift
SOURCE_STAGE="${1:-}"
;;
-h|--help)
usage
exit 0
;;
*) fail "unknown_argument" ;;
esac
shift
done
case "$MODE" in check|apply|verify) ;; *) fail "invalid_mode" ;; esac
[[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] || fail "invalid_source_revision"
[[ "$RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$ ]] || fail "invalid_run_id"
expected_source_stage="/tmp/agent99-host110-backup-runtime-${RUN_ID}"
[ "$SOURCE_STAGE" = "$expected_source_stage" ] || fail "invalid_source_stage"
RECEIPT_PATH="$STATUS_ROOT/agent99-backup-runtime-deploy-${RUN_ID}.json"
TERMINAL_RECEIPT_PATH="$STATUS_ROOT/agent99-backup-runtime-terminal-${RUN_ID}.json"
if [ "${HOST110_BACKUP_RUNTIME_ENABLE_TEST_FAULTS:-}" = "1" ] \
&& [ "$DEST_ROOT" != "/backup/scripts" ]; then
FAULT_INJECTION_ENABLED=1
fi
if [ "$FAULT_INJECTION_ENABLED" -eq 1 ]; then
case "${HOST110_BACKUP_RUNTIME_TEST_COMMIT_SIGNAL:-}" in ""|INT|HUP|TERM) ;; *) fail "invalid_test_commit_signal" ;; esac
case "${HOST110_BACKUP_RUNTIME_TEST_TERMINAL_SIGNAL:-}" in ""|INT|HUP|TERM) ;; *) fail "invalid_test_terminal_signal" ;; esac
case "${HOST110_BACKUP_RUNTIME_TEST_TERMINAL_RECEIPT_FAULT:-}" in ""|write|link|fsync|readback) ;; *) fail "invalid_test_terminal_receipt_fault" ;; esac
case "${HOST110_BACKUP_RUNTIME_TEST_CLEANUP_FAULT:-}" in ""|stage|rollback_prestate) ;; *) fail "invalid_test_cleanup_fault" ;; esac
fi
for command_name in awk bash cp dirname flock grep hostname id install mv pgrep python3 readlink rm sha256sum stat timeout tr; do
command -v "$command_name" >/dev/null 2>&1 || fail "required_command_missing_${command_name}"
done
current_user="$(id -un)"
[ "$current_user" = "$EXPECTED_USER" ] || fail "unexpected_executor_user"
hostname -I 2>/dev/null | tr ' ' '\n' | grep -qx "$EXPECTED_HOST_IP" || fail "unexpected_executor_host"
[ -d "$SOURCE_STAGE" ] && [ ! -L "$SOURCE_STAGE" ] || fail "canonical_source_stage_unavailable"
[ -d "$DEST_ROOT" ] && [ ! -L "$DEST_ROOT" ] || fail "backup_runtime_destination_unavailable"
[ -d "$EXPORTER_ROOT" ] && [ ! -L "$EXPORTER_ROOT" ] || fail "backup_exporter_destination_unavailable"
manifest_rows="$(python3 - "$SOURCE_STAGE/manifest.json" "$SOURCE_REVISION" "$RUN_ID" "${PAYLOAD_FILES[@]}" <<'PY'
import json
import re
import sys
from pathlib import Path
path = Path(sys.argv[1])
source_revision = sys.argv[2]
run_id = sys.argv[3]
expected_names = sys.argv[4:]
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
raise SystemExit(65)
rows = document.get("files")
if (
document.get("schemaVersion") != "agent99_host110_backup_runtime_source_manifest_v1"
or document.get("sourceRevision") != source_revision
or document.get("runId") != run_id
or not re.fullmatch(r"[0-9a-f]{40}", str(document.get("sourceHead", "")))
or not re.fullmatch(r"[0-9a-f]{64}", str(document.get("executorSha256", "")))
or not re.fullmatch(r"[0-9a-f]{64}", str(document.get("verifierSha256", "")))
or not isinstance(rows, list)
or len(rows) != len(expected_names)
):
raise SystemExit(66)
by_name = {}
for row in rows:
if not isinstance(row, dict):
raise SystemExit(66)
name = str(row.get("name", ""))
digest = str(row.get("sha256", ""))
if name in by_name or not re.fullmatch(r"[A-Za-z0-9_.-]+", name) or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise SystemExit(66)
by_name[name] = digest
if set(by_name) != set(expected_names):
raise SystemExit(66)
print(f"SOURCE_HEAD {document['sourceHead']}")
print(f"EXECUTOR_SHA256 {document['executorSha256']}")
print(f"VERIFIER_SHA256 {document['verifierSha256']}")
for name in expected_names:
print(f"{name} {by_name[name]}")
PY
)" || fail "source_manifest_invalid"
while read -r manifest_name manifest_digest; do
case "$manifest_name" in
SOURCE_HEAD) SOURCE_HEAD="$manifest_digest" ;;
EXECUTOR_SHA256) EXECUTOR_DIGEST="$manifest_digest" ;;
VERIFIER_SHA256) VERIFIER_DIGEST="$manifest_digest" ;;
*) EXPECTED_DIGEST[$manifest_name]="$manifest_digest" ;;
esac
done <<< "$manifest_rows"
[[ "$SOURCE_HEAD" =~ ^[0-9a-f]{40}$ ]] || fail "source_head_invalid"
working_digest() {
sha256sum "$1" | awk '{print $1}'
}
[ "$(working_digest "$0")" = "$EXECUTOR_DIGEST" ] || fail "executor_identity_failed"
[ -f "$SOURCE_STAGE/verify-host110-backup-runtime.py" ] \
&& [ ! -L "$SOURCE_STAGE/verify-host110-backup-runtime.py" ] \
&& [ "$(working_digest "$SOURCE_STAGE/verify-host110-backup-runtime.py")" = "$VERIFIER_DIGEST" ] \
|| fail "verifier_identity_failed"
validate_file() {
local validation_path="$1"
case "$validation_path" in
*.sh) bash -n "$validation_path" ;;
*.py) python3 -c 'import ast, pathlib, sys; ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))' "$validation_path" ;;
*) return 64 ;;
esac
}
source_digest() {
local name="$1"
printf '%s\n' "${EXPECTED_DIGEST[$name]:-}"
}
validate_source() {
local name source_path expected_digest actual_digest resolved
for name in "${PAYLOAD_FILES[@]}"; do
source_path="$SOURCE_STAGE/$name"
[ -f "$source_path" ] && [ ! -L "$source_path" ] || return 65
resolved="$(readlink -f "$source_path")"
[[ "$resolved" == "$SOURCE_STAGE/"* ]] || return 65
expected_digest="$(source_digest "$name")"
actual_digest="$(working_digest "$source_path")"
[ "$actual_digest" = "$expected_digest" ] || return 66
validate_file "$source_path" || return 67
done
}
destination_path() {
local name="$1"
if [ "$name" = "$EXPORTER_FILE" ]; then
printf '%s/%s\n' "$EXPORTER_ROOT" "$name"
else
printf '%s/%s\n' "$DEST_ROOT" "$name"
fi
}
verify_destination() {
local name dest_path expected_digest actual_digest
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] || return 68
expected_digest="$(source_digest "$name")"
actual_digest="$(working_digest "$dest_path")"
[ "$actual_digest" = "$expected_digest" ] || return 69
[ "$(stat -c '%U:%G:%a' "$dest_path")" = "wooo:wooo:755" ] || return 70
done
}
fsync_paths_and_parents() {
python3 - "$@" <<'PY'
import os
import stat
import sys
from pathlib import Path
parents = set()
for raw_path in sys.argv[1:]:
path = Path(raw_path)
parents.add(path.parent)
if not os.path.lexists(path):
continue
if path.is_symlink() or not path.is_file():
raise SystemExit(76)
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
try:
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
raise SystemExit(76)
os.fsync(descriptor)
finally:
os.close(descriptor)
for parent in sorted(parents, key=str):
if parent.is_symlink() or not parent.is_dir():
raise SystemExit(76)
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(parent, flags)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
PY
}
fsync_destination_state() {
local name dest_path
local -a fsync_targets=()
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
fsync_targets+=("$dest_path")
done
fsync_paths_and_parents "${fsync_targets[@]}"
}
fsync_rollback_prestate() {
local name
local -a fsync_targets=("$ROLLBACK_DIR/prestate.tsv")
for name in "${PAYLOAD_FILES[@]}"; do
if [ "${FILE_EXISTED[$name]:-}" = "1" ]; then
fsync_targets+=("$ROLLBACK_DIR/$name")
fi
done
fsync_paths_and_parents "${fsync_targets[@]}"
}
write_receipt() {
local receipt_status="$1"
local rollback_attempted="$2"
local rollback_performed="$3"
local rollback_verified="$4"
local zero_residue_verified="$5"
install -d -m 700 "$STATUS_ROOT" || return 1
[ -d "$STATUS_ROOT" ] && [ ! -L "$STATUS_ROOT" ] || return 1
[ "$(stat -c '%U:%G:%a' "$STATUS_ROOT")" = "wooo:wooo:700" ] || return 1
if ! python3 - "$RECEIPT_PATH" "$receipt_status" "$SOURCE_REVISION" "$SOURCE_HEAD" "$RUN_ID" \
"$rollback_attempted" "$rollback_performed" "$rollback_verified" \
"$zero_residue_verified" "$VERIFIER_DIGEST" "$FAULT_INJECTION_ENABLED" <<'PY'
import hashlib
import json
import os
import sys
import time
from pathlib import Path
path = Path(sys.argv[1])
status = sys.argv[2]
rollback_attempted = sys.argv[6] == "1"
rollback_performed = sys.argv[7] == "1"
rollback_verified = sys.argv[8] == "1"
zero_residue_verified = sys.argv[9] == "1"
if status not in {"payload_verified", "failed_rolled_back", "rollback_unverified"}:
raise SystemExit(78)
if status == "payload_verified" and (rollback_attempted or rollback_performed or rollback_verified or not zero_residue_verified):
raise SystemExit(78)
if status == "failed_rolled_back" and not (
rollback_attempted and rollback_performed and rollback_verified and zero_residue_verified
):
raise SystemExit(78)
if status == "rollback_unverified" and (not rollback_attempted or rollback_verified):
raise SystemExit(78)
document = {
"schemaVersion": "agent99_host110_backup_runtime_deploy_receipt_v3",
"status": status,
"sourceRevision": sys.argv[3],
"sourceHead": sys.argv[4],
"runId": sys.argv[5],
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"rollbackAttempted": rollback_attempted,
"rollbackPerformed": rollback_performed,
"rollbackVerified": rollback_verified,
"zeroResidueVerified": zero_residue_verified,
"zeroResidueScope": "run_owned_destination_temps",
"verifierSha256": sys.argv[10],
"executorHost": "192.168.0.110",
"productionServiceRestarted": False,
"secretValuesRead": False,
"writtenAt": int(time.time()),
}
payload = (json.dumps(document, ensure_ascii=True, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
expected_digest = hashlib.sha256(payload).hexdigest()
temporary = path.with_name(f".{path.name}.tmp-{os.getpid()}")
fault_enabled = sys.argv[11] == "1"
fault = (
os.environ.get("HOST110_BACKUP_RUNTIME_TEST_RECEIPT_FAULT", "")
if status == "payload_verified" and fault_enabled
else ""
)
if fault not in {"", "write", "link", "fsync", "readback"}:
raise SystemExit(78)
created_final = False
directory_fd = None
try:
if os.path.lexists(path) or os.path.lexists(temporary):
raise FileExistsError(path)
if fault == "write":
raise OSError("fault_injected_receipt_write")
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "wb") as handle:
if handle.write(payload) != len(payload):
raise OSError("receipt_short_write")
handle.flush()
if fault == "fsync":
raise OSError("fault_injected_receipt_fsync")
os.fsync(handle.fileno())
os.chmod(temporary, 0o600)
temporary_readback = temporary.read_bytes()
if temporary_readback != payload or hashlib.sha256(temporary_readback).hexdigest() != expected_digest:
raise OSError("receipt_temporary_readback_failed")
if fault == "link":
raise OSError("fault_injected_receipt_link")
os.link(temporary, path)
created_final = True
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_fd = os.open(path.parent, directory_flags)
os.fsync(directory_fd)
if fault == "readback":
raise OSError("fault_injected_receipt_readback")
readback = path.read_bytes()
if readback != payload or hashlib.sha256(readback).hexdigest() != expected_digest:
raise OSError("receipt_final_readback_failed")
if json.loads(readback.decode("utf-8")) != document:
raise OSError("receipt_document_readback_failed")
temporary.unlink()
os.fsync(directory_fd)
except BaseException:
try:
if created_final:
path.unlink(missing_ok=True)
temporary.unlink(missing_ok=True)
if directory_fd is None and path.parent.is_dir():
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_fd = os.open(path.parent, directory_flags)
if directory_fd is not None:
os.fsync(directory_fd)
finally:
raise
finally:
if directory_fd is not None:
os.close(directory_fd)
PY
then
return 1
fi
[ -f "$RECEIPT_PATH" ] && [ ! -L "$RECEIPT_PATH" ] || return 1
return 0
}
write_terminal_receipt() {
local terminal_status="$1"
local stage_cleanup_verified="$2"
local rollback_prestate_cleanup_verified="$3"
local deferred_signal="$4"
local terminal_exit_code="$5"
install -d -m 700 "$STATUS_ROOT" || return 1
[ -d "$STATUS_ROOT" ] && [ ! -L "$STATUS_ROOT" ] || return 1
[ "$(stat -c '%U:%G:%a' "$STATUS_ROOT")" = "wooo:wooo:700" ] || return 1
if ! python3 - "$TERMINAL_RECEIPT_PATH" "$RECEIPT_PATH" "$terminal_status" \
"$SOURCE_REVISION" "$SOURCE_HEAD" "$RUN_ID" "$stage_cleanup_verified" \
"$rollback_prestate_cleanup_verified" "$deferred_signal" "$terminal_exit_code" \
"$VERIFIER_DIGEST" "$FAULT_INJECTION_ENABLED" <<'PY'
import hashlib
import json
import os
import signal
import sys
import time
from pathlib import Path
path = Path(sys.argv[1])
payload_receipt_path = Path(sys.argv[2])
status = sys.argv[3]
source_revision = sys.argv[4]
source_head = sys.argv[5]
run_id = sys.argv[6]
stage_cleanup_verified = sys.argv[7] == "1"
rollback_prestate_cleanup_verified = sys.argv[8] == "1"
deferred_signal = sys.argv[9]
terminal_exit_code = int(sys.argv[10])
verifier_digest = sys.argv[11]
fault_enabled = sys.argv[12] == "1"
signal_exit_codes = {"INT": 130, "HUP": 129, "TERM": 143}
terminal_signal = os.environ.get("HOST110_BACKUP_RUNTIME_TEST_TERMINAL_SIGNAL", "") if fault_enabled else ""
terminal_fault = os.environ.get("HOST110_BACKUP_RUNTIME_TEST_TERMINAL_RECEIPT_FAULT", "") if fault_enabled else ""
if terminal_signal:
os.kill(os.getppid(), getattr(signal, f"SIG{terminal_signal}"))
time.sleep(0.05)
try:
payload_receipt = json.loads(payload_receipt_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
raise SystemExit(79)
if not (
payload_receipt.get("schemaVersion") == "agent99_host110_backup_runtime_deploy_receipt_v3"
and payload_receipt.get("status") == "payload_verified"
and payload_receipt.get("sourceRevision") == source_revision
and payload_receipt.get("sourceHead") == source_head
and payload_receipt.get("runId") == run_id
and payload_receipt.get("zeroResidueVerified") is True
):
raise SystemExit(79)
if status == "verified":
valid = (
stage_cleanup_verified
and rollback_prestate_cleanup_verified
and not deferred_signal
and terminal_exit_code == 0
)
elif status == "cleanup_pending":
valid = (
not (stage_cleanup_verified and rollback_prestate_cleanup_verified)
and deferred_signal in {"", *signal_exit_codes}
and terminal_exit_code != 0
)
elif status == "committed_signal_deferred":
valid = (
stage_cleanup_verified
and rollback_prestate_cleanup_verified
and deferred_signal in signal_exit_codes
and terminal_exit_code == signal_exit_codes[deferred_signal]
)
else:
valid = False
if not valid:
raise SystemExit(79)
document = {
"schemaVersion": "agent99_host110_backup_runtime_terminal_receipt_v1",
"status": status,
"ok": status == "verified",
"payloadCommitted": True,
"payloadReceipt": str(payload_receipt_path),
"sourceRevision": source_revision,
"sourceHead": source_head,
"runId": run_id,
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"stageCleanupVerified": stage_cleanup_verified,
"rollbackPrestateCleanupVerified": rollback_prestate_cleanup_verified,
"cleanupVerified": stage_cleanup_verified and rollback_prestate_cleanup_verified,
"independentVerifierVerified": True,
"zeroResidueScope": "run_owned_destination_temps_and_internal_stage_prestate",
"deferredSignal": deferred_signal or None,
"terminalExitCode": terminal_exit_code,
"verifierSha256": verifier_digest,
"executorHost": "192.168.0.110",
"productionServiceRestarted": False,
"secretValuesRead": False,
"writtenAt": int(time.time()),
}
payload = (json.dumps(document, ensure_ascii=True, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
expected_digest = hashlib.sha256(payload).hexdigest()
temporary = path.with_name(f".{path.name}.tmp-{os.getpid()}")
created_final = False
directory_fd = None
try:
if os.path.lexists(path) or os.path.lexists(temporary):
raise FileExistsError(path)
if terminal_fault == "write":
raise OSError("fault_injected_terminal_receipt_write")
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "wb") as handle:
if handle.write(payload) != len(payload):
raise OSError("terminal_receipt_short_write")
handle.flush()
if terminal_fault == "fsync":
raise OSError("fault_injected_terminal_receipt_fsync")
os.fsync(handle.fileno())
os.chmod(temporary, 0o600)
temporary_readback = temporary.read_bytes()
if temporary_readback != payload or hashlib.sha256(temporary_readback).hexdigest() != expected_digest:
raise OSError("terminal_receipt_temporary_readback_failed")
if terminal_fault == "link":
raise OSError("fault_injected_terminal_receipt_link")
os.link(temporary, path)
created_final = True
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_fd = os.open(path.parent, directory_flags)
os.fsync(directory_fd)
if terminal_fault == "readback":
raise OSError("fault_injected_terminal_receipt_readback")
readback = path.read_bytes()
if readback != payload or hashlib.sha256(readback).hexdigest() != expected_digest:
raise OSError("terminal_receipt_final_readback_failed")
if json.loads(readback.decode("utf-8")) != document:
raise OSError("terminal_receipt_document_readback_failed")
temporary.unlink()
os.fsync(directory_fd)
except BaseException:
try:
if created_final:
path.unlink(missing_ok=True)
temporary.unlink(missing_ok=True)
if directory_fd is None and path.parent.is_dir():
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_fd = os.open(path.parent, directory_flags)
if directory_fd is not None:
os.fsync(directory_fd)
finally:
raise
finally:
if directory_fd is not None:
os.close(directory_fd)
PY
then
return 1
fi
[ -f "$TERMINAL_RECEIPT_PATH" ] && [ ! -L "$TERMINAL_RECEIPT_PATH" ] || return 1
return 0
}
record_prestate() {
local name dest_path digest metadata
: > "$ROLLBACK_DIR/prestate.tsv" || return 74
chmod 600 "$ROLLBACK_DIR/prestate.tsv" || return 74
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
if [ -f "$dest_path" ] && [ ! -L "$dest_path" ]; then
[ "$(stat -c '%U:%G' "$dest_path")" = "wooo:wooo" ] || return 71
FILE_EXISTED[$name]=1
digest="$(working_digest "$dest_path")" || return 72
metadata="$(stat -c '%u:%g:%a' "$dest_path")" || return 72
PREVIOUS_DIGEST[$name]="$digest"
PREVIOUS_METADATA[$name]="$metadata"
printf '%s\t1\t%s\t%s\n' "$name" "$digest" "$metadata" >> "$ROLLBACK_DIR/prestate.tsv" || return 72
cp -p -- "$dest_path" "$ROLLBACK_DIR/$name" || return 72
[ "$(working_digest "$ROLLBACK_DIR/$name")" = "$digest" ] || return 72
elif [ ! -e "$dest_path" ]; then
FILE_EXISTED[$name]=0
PREVIOUS_DIGEST[$name]="-"
PREVIOUS_METADATA[$name]="-"
printf '%s\t0\t-\t-\n' "$name" >> "$ROLLBACK_DIR/prestate.tsv" || return 72
else
return 73
fi
done
fsync_rollback_prestate || return 75
}
register_run_owned_temp() {
RUN_OWNED_TEMP_PATHS["$1"]=1
}
verify_run_owned_temp_absence() {
local temporary
for temporary in "${!RUN_OWNED_TEMP_PATHS[@]}"; do
[ ! -e "$temporary" ] && [ ! -L "$temporary" ] || return 1
done
}
cleanup_run_owned_temps() {
local temporary failed=0 preserved=0
local fault=""
if [ "$FAULT_INJECTION_ENABLED" -eq 1 ]; then
fault="${HOST110_BACKUP_RUNTIME_TEST_ROLLBACK_FAULT:-}"
fi
for temporary in "${!RUN_OWNED_TEMP_PATHS[@]}"; do
if [ "$fault" = "preserve_first_temp" ] \
&& [ "$preserved" -eq 0 ] \
&& { [ -e "$temporary" ] || [ -L "$temporary" ]; }; then
preserved=1
failed=1
continue
fi
rm -f -- "$temporary" || failed=1
done
verify_run_owned_temp_absence || failed=1
[ "$failed" -eq 0 ]
}
rollback_transaction() {
local name dest_path destination_parent backup_path temporary failed=0
ROLLBACK_ATTEMPTED=1
ROLLBACK_PERFORMED=0
ROLLBACK_VERIFIED=0
RUN_TEMP_RESIDUE_VERIFIED=0
set +e
cleanup_run_owned_temps || failed=1
for name in "${PAYLOAD_FILES[@]}"; do
[ -n "${FILE_EXISTED[$name]+x}" ] || { failed=1; continue; }
dest_path="$(destination_path "$name")"
if [ "${FILE_EXISTED[$name]}" = "1" ]; then
backup_path="$ROLLBACK_DIR/$name"
destination_parent="$(dirname "$dest_path")"
temporary="$destination_parent/.${name}.agent99-rollback-${RUN_ID}"
register_run_owned_temp "$temporary"
if cp -p -- "$backup_path" "$temporary" && mv -f -- "$temporary" "$dest_path"; then
ROLLBACK_PERFORMED=1
else
failed=1
fi
else
if rm -f -- "$dest_path"; then
ROLLBACK_PERFORMED=1
else
failed=1
fi
fi
done
cleanup_run_owned_temps || failed=1
fsync_destination_state || failed=1
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
if [ "${FILE_EXISTED[$name]:-}" = "1" ]; then
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] \
&& [ "$(working_digest "$dest_path" 2>/dev/null)" = "${PREVIOUS_DIGEST[$name]:-invalid}" ] \
&& [ "$(stat -c '%u:%g:%a' "$dest_path" 2>/dev/null)" = "${PREVIOUS_METADATA[$name]:-invalid}" ] \
|| failed=1
elif [ "${FILE_EXISTED[$name]:-}" = "0" ]; then
[ ! -e "$dest_path" ] || failed=1
else
failed=1
fi
done
if verify_run_owned_temp_absence; then
RUN_TEMP_RESIDUE_VERIFIED=1
else
failed=1
RUN_TEMP_RESIDUE_VERIFIED=0
fi
set -e
if [ "$failed" -eq 0 ] && [ "$ROLLBACK_PERFORMED" -eq 1 ] && [ "$RUN_TEMP_RESIDUE_VERIFIED" -eq 1 ]; then
ROLLBACK_VERIFIED=1
return 0
fi
ROLLBACK_VERIFIED=0
return 1
}
handle_agent99_signal() {
local signal_name="$1"
local signal_exit_code="$2"
if [ "$COMMIT_CRITICAL" -eq 1 ] || [ "$APPLY_COMPLETE" -eq 1 ]; then
if [ -z "$DEFERRED_SIGNAL" ]; then
DEFERRED_SIGNAL="$signal_name"
DEFERRED_SIGNAL_EXIT="$signal_exit_code"
fi
return 0
fi
exit "$signal_exit_code"
}
cleanup_postcommit_directory() {
local cleanup_kind="$1"
local cleanup_path="$2"
local fault=""
if [ "$FAULT_INJECTION_ENABLED" -eq 1 ]; then
fault="${HOST110_BACKUP_RUNTIME_TEST_CLEANUP_FAULT:-}"
fi
if [ "$fault" = "$cleanup_kind" ]; then
return 1
fi
rm -rf -- "$cleanup_path" || return 1
[ ! -e "$cleanup_path" ] && [ ! -L "$cleanup_path" ] || return 1
fsync_paths_and_parents "$cleanup_path" || return 1
[ ! -e "$cleanup_path" ] && [ ! -L "$cleanup_path" ]
}
cleanup() {
local exit_status=$?
local rollback_status="rollback_unverified"
local failure_receipt_written=0
trap - EXIT HUP INT TERM
if [ "$APPLY_STARTED" -eq 1 ] && [ "$APPLY_COMPLETE" -ne 1 ]; then
if rollback_transaction; then
rollback_status="failed_rolled_back"
fi
if write_receipt "$rollback_status" "$ROLLBACK_ATTEMPTED" "$ROLLBACK_PERFORMED" \
"$ROLLBACK_VERIFIED" "$RUN_TEMP_RESIDUE_VERIFIED"; then
failure_receipt_written=1
fi
fi
if [ "$APPLY_COMPLETE" -ne 1 ] && [ -n "$STAGE_DIR" ]; then
rm -rf -- "$STAGE_DIR" || true
fi
if [ "$APPLY_COMPLETE" -ne 1 ] \
&& [ "$ROLLBACK_VERIFIED" -eq 1 ] \
&& [ "$failure_receipt_written" -eq 1 ]; then
if [ -n "$ROLLBACK_DIR" ]; then
rm -rf -- "$ROLLBACK_DIR" || true
fi
fi
exit "$exit_status"
}
validate_source || fail "source_validation_failed"
if [ "$MODE" = "check" ]; then
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"check","ok":true,"sourceRevision":"%s","sourceHead":"%s","runId":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD" "$RUN_ID"
exit 0
fi
if [ "$MODE" = "verify" ]; then
verify_destination || fail "destination_verification_failed"
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"verify","ok":true,"sourceRevision":"%s","sourceHead":"%s","runId":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD" "$RUN_ID"
exit 0
fi
[ ! -e "$RECEIPT_PATH" ] && [ ! -L "$RECEIPT_PATH" ] || fail "run_identity_receipt_exists"
[ ! -e "$TERMINAL_RECEIPT_PATH" ] && [ ! -L "$TERMINAL_RECEIPT_PATH" ] \
|| fail "run_identity_terminal_receipt_exists"
STAGE_DIR="$STAGE_ROOT/$RUN_ID"
ROLLBACK_DIR="$ROLLBACK_ROOT/$RUN_ID"
[ ! -e "$STAGE_DIR" ] || fail "run_identity_stage_exists"
[ ! -e "$ROLLBACK_DIR" ] || fail "run_identity_rollback_exists"
[ ! -L "$LOCK_PATH" ] || fail "deployment_lock_symlink_unsafe"
(umask 077; : >> "$LOCK_PATH") || fail "deployment_lock_unavailable"
[ -f "$LOCK_PATH" ] && [ -O "$LOCK_PATH" ] || fail "deployment_lock_owner_invalid"
exec 9>>"$LOCK_PATH"
flock -x -w 30 9 || fail "deployment_lock_timeout"
# Old runtime revisions predate the shared gate. Refuse the first promotion if
# one of them is already active; after common.sh moves first, new starts block.
if pgrep -afu "$EXPECTED_USER" '/backup/scripts/(backup-|check-backup|sync-offsite|verify-offsite|enforce-latest)' \
| grep -vF "host110-backup-runtime-executor.sh" >/dev/null 2>&1; then
fail "legacy_backup_runtime_active"
fi
install -d -m 700 "$STAGE_DIR" "$ROLLBACK_DIR"
trap cleanup EXIT
trap 'handle_agent99_signal INT 130' INT
trap 'handle_agent99_signal HUP 129' HUP
trap 'handle_agent99_signal TERM 143' TERM
for name in "${PAYLOAD_FILES[@]}"; do
install -m 700 "$SOURCE_STAGE/$name" "$STAGE_DIR/$name"
[ "$(working_digest "$STAGE_DIR/$name")" = "$(source_digest "$name")" ] || fail "stage_identity_failed"
validate_file "$STAGE_DIR/$name" || fail "stage_validation_failed"
done
record_prestate || fail "rollback_prestate_capture_failed"
APPLY_STARTED=1
# common.sh is promoted first. Every managed mutating backup entrypoint then
# takes the shared side of LOCK_PATH before doing work, while this process holds
# the exclusive side until the independent verifier has completed.
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
destination_parent="$(dirname "$dest_path")"
temporary="$destination_parent/.${name}.agent99-${RUN_ID}"
register_run_owned_temp "$temporary"
install -m 755 "$STAGE_DIR/$name" "$temporary"
if [ "$FAULT_INJECTION_ENABLED" -eq 1 ] \
&& [ "${HOST110_BACKUP_RUNTIME_TEST_APPLY_FAULT:-}" = "after_temp_install" ] \
&& [ "$name" = "${PAYLOAD_FILES[0]}" ]; then
fail "fault_injected_after_temp_install"
fi
mv -f -- "$temporary" "$dest_path"
done
fsync_destination_state || fail "post_apply_durability_failed"
verify_destination || fail "post_apply_verification_failed"
verifier_result="$(BACKUP_RUNTIME_DEPLOY_CONTEXT=1 python3 "$SOURCE_STAGE/verify-host110-backup-runtime.py" \
--manifest "$SOURCE_STAGE/manifest.json" \
--destination "$DEST_ROOT" \
--exporter-destination "$EXPORTER_ROOT/$EXPORTER_FILE" \
--source-revision "$SOURCE_REVISION" \
--run-id "$RUN_ID" \
--expected-verifier-sha256 "$VERIFIER_DIGEST")" || fail "independent_verifier_failed"
python3 - "$verifier_result" "$SOURCE_REVISION" "$RUN_ID" <<'PY' || fail "independent_verifier_contract_failed"
import json
import sys
try:
row = json.loads(sys.argv[1])
except json.JSONDecodeError:
raise SystemExit(1)
if not (
row.get("schemaVersion") == "agent99_host110_backup_runtime_verifier_v1"
and row.get("ok") is True
and row.get("sourceRevision") == sys.argv[2]
and row.get("runId") == sys.argv[3]
and row.get("fileCount") == 18
and row.get("backupScriptFileCount") == 17
and row.get("backupHealthExporterIncluded") is True
and row.get("remoteWritePerformed") is False
and row.get("selfIdentityVerified") is True
):
raise SystemExit(1)
PY
verify_run_owned_temp_absence || fail "post_apply_temp_residue_detected"
RUN_TEMP_RESIDUE_VERIFIED=1
COMMIT_CRITICAL=1
if ! write_receipt "payload_verified" 0 0 0 "$RUN_TEMP_RESIDUE_VERIFIED"; then
COMMIT_CRITICAL=0
fail "durable_receipt_failed"
fi
if [ "$FAULT_INJECTION_ENABLED" -eq 1 ] \
&& [ -n "${HOST110_BACKUP_RUNTIME_TEST_COMMIT_SIGNAL:-}" ]; then
case "$HOST110_BACKUP_RUNTIME_TEST_COMMIT_SIGNAL" in
INT|HUP|TERM) kill -s "$HOST110_BACKUP_RUNTIME_TEST_COMMIT_SIGNAL" "$$" ;;
esac
fi
APPLY_COMPLETE=1
COMMIT_CRITICAL=0
stage_cleanup_verified=0
rollback_prestate_cleanup_verified=0
if cleanup_postcommit_directory "stage" "$STAGE_DIR"; then
stage_cleanup_verified=1
fi
if cleanup_postcommit_directory "rollback_prestate" "$ROLLBACK_DIR"; then
rollback_prestate_cleanup_verified=1
fi
terminal_status="verified"
terminal_exit_code=0
# Cleanup has completed. Freeze terminal signal state before the immutable
# terminal writer so its receipt, stdout, and exit status cannot diverge.
trap '' HUP INT TERM
if [ "$stage_cleanup_verified" -ne 1 ] || [ "$rollback_prestate_cleanup_verified" -ne 1 ]; then
terminal_status="cleanup_pending"
terminal_exit_code=75
elif [ -n "$DEFERRED_SIGNAL" ]; then
terminal_status="committed_signal_deferred"
terminal_exit_code="$DEFERRED_SIGNAL_EXIT"
fi
if ! write_terminal_receipt "$terminal_status" "$stage_cleanup_verified" \
"$rollback_prestate_cleanup_verified" "$DEFERRED_SIGNAL" "$terminal_exit_code"; then
trap - EXIT
python3 - "$SOURCE_REVISION" "$SOURCE_HEAD" "$RUN_ID" "$RECEIPT_PATH" \
"$TERMINAL_RECEIPT_PATH" "$stage_cleanup_verified" \
"$rollback_prestate_cleanup_verified" "$DEFERRED_SIGNAL" <<'PY'
import json
import sys
print(json.dumps({
"schemaVersion": "agent99_host110_backup_runtime_executor_v1",
"mode": "apply",
"status": "terminal_receipt_failed",
"ok": False,
"sourceRevision": sys.argv[1],
"sourceHead": sys.argv[2],
"runId": sys.argv[3],
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"payloadCommitted": True,
"rollbackAttempted": False,
"rollbackPerformed": False,
"rollbackVerified": False,
"zeroResidueVerified": True,
"zeroResidueScope": "run_owned_destination_temps",
"stageCleanupVerified": sys.argv[6] == "1",
"rollbackPrestateCleanupVerified": sys.argv[7] == "1",
"receipt": sys.argv[4],
"terminalReceipt": sys.argv[5],
"terminalReceiptPublished": False,
"terminalExitCode": 76,
"deferredSignal": sys.argv[8] or None,
"independentVerifierVerified": True,
"verifier": None,
}, ensure_ascii=True, sort_keys=True))
PY
printf 'HOST110_BACKUP_RUNTIME_OK=0\nERROR=terminal_receipt_failed\n' >&2
exit 76
fi
if [ "$stage_cleanup_verified" -eq 1 ]; then STAGE_DIR=""; fi
if [ "$rollback_prestate_cleanup_verified" -eq 1 ]; then ROLLBACK_DIR=""; fi
trap - EXIT
python3 - "$SOURCE_REVISION" "$SOURCE_HEAD" "$RECEIPT_PATH" "$TERMINAL_RECEIPT_PATH" \
"$verifier_result" "$stage_cleanup_verified" "$rollback_prestate_cleanup_verified" \
"$terminal_status" "$terminal_exit_code" "$DEFERRED_SIGNAL" "$RUN_ID" <<'PY'
import json
import sys
status = sys.argv[8]
verifier = json.loads(sys.argv[5])
print(json.dumps({
"schemaVersion": "agent99_host110_backup_runtime_executor_v1",
"mode": "apply",
"status": status,
"ok": status == "verified",
"sourceRevision": sys.argv[1],
"sourceHead": sys.argv[2],
"runId": sys.argv[11],
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"payloadCommitted": True,
"rollbackAttempted": False,
"rollbackPerformed": False,
"rollbackVerified": False,
"zeroResidueVerified": True,
"zeroResidueScope": "run_owned_destination_temps",
"stageCleanupVerified": sys.argv[6] == "1",
"rollbackPrestateCleanupVerified": sys.argv[7] == "1",
"receipt": sys.argv[3],
"terminalReceipt": sys.argv[4],
"terminalReceiptPublished": True,
"terminalExitCode": int(sys.argv[9]),
"deferredSignal": sys.argv[10] or None,
"independentVerifierVerified": True,
"verifier": verifier if status == "verified" else None,
}, ensure_ascii=True, sort_keys=True))
PY
exit "$terminal_exit_code"