From 12fc4b8914fe1c0c93be7705f2c8a9b712b14cc7 Mon Sep 17 00:00:00 2001 From: ogt Date: Tue, 14 Jul 2026 16:02:33 +0800 Subject: [PATCH] fix(host112): persist installer post-gate blockers --- .../install-host112-guest-recovery.sh | 24 ++++++++++++++++++- ...install_host112_guest_recovery_contract.py | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/scripts/reboot-recovery/install-host112-guest-recovery.sh b/scripts/reboot-recovery/install-host112-guest-recovery.sh index b7cef9c8d..36553244e 100755 --- a/scripts/reboot-recovery/install-host112-guest-recovery.sh +++ b/scripts/reboot-recovery/install-host112-guest-recovery.sh @@ -43,6 +43,7 @@ INSTALL_RECEIPT_REF="none" INSTALL_RECEIPT_PATH="" INSTALL_ATTEMPT_ID="" BACKUP_DIR="" +STATIC_POST_GATE_BLOCKERS="not_evaluated" sudoers_tmp="" AUTH_TMP="" TIMER_ACTIVATION_PERFORMED=0 @@ -441,7 +442,7 @@ write_install_receipt() { installer_sha="${installer_sha:-missing}" receipt_tmp="${INSTALL_RECEIPT_PATH}.tmp.$$" printf '%s\n' \ - "schema_version=host112_guest_recovery_install_receipt_v1 generated_at=$generated_at trace_id=$TRACE_ID run_id=$RUN_ID work_item_id=$WORK_ITEM_ID terminal=$terminal install_verified=$install_verified installer_runtime_write_performed=1 manager_runtime_write_performed=0 static_post_gate_ready=${STATIC_POST_GATE_READY:-0} check_gate_ready=${CHECK_GATE_READY:-0} sudo_policy_ready=${SUDO_POLICY_READY:-0} agent99_99_direct_route_ready=${AGENT99_99_DIRECT_ROUTE_READY:-0} agent99_99_direct_runtime_probe=not_started timer_enabled=${TIMER_ENABLED:-unknown} timer_active=${TIMER_ACTIVE:-unknown} timer_activation_performed=$TIMER_ACTIVATION_PERFORMED synchronous_recovery_start=0 recovery_execution_performed=0 recovery_run_id=none recovery_receipt_ref=none rollback_attempted=$rollback_attempted rollback_verified=$rollback_verified error_exit=$error_exit backup_ref=$BACKUP_DIR readiness_sha256=$readiness_sha installer_sha256=$installer_sha prohibited_actions=host_reboot,vm_power_change,vm_reset,firewall_change,secret_read,database_write" \ + "schema_version=host112_guest_recovery_install_receipt_v1 generated_at=$generated_at trace_id=$TRACE_ID run_id=$RUN_ID work_item_id=$WORK_ITEM_ID terminal=$terminal install_verified=$install_verified installer_runtime_write_performed=1 manager_runtime_write_performed=0 static_post_gate_ready=${STATIC_POST_GATE_READY:-0} static_post_gate_blockers=${STATIC_POST_GATE_BLOCKERS:-not_evaluated} check_gate_ready=${CHECK_GATE_READY:-0} sudo_policy_ready=${SUDO_POLICY_READY:-0} agent99_99_direct_route_ready=${AGENT99_99_DIRECT_ROUTE_READY:-0} agent99_99_direct_runtime_probe=not_started timer_enabled=${TIMER_ENABLED:-unknown} timer_active=${TIMER_ACTIVE:-unknown} timer_activation_performed=$TIMER_ACTIVATION_PERFORMED synchronous_recovery_start=0 recovery_execution_performed=0 recovery_run_id=none recovery_receipt_ref=none rollback_attempted=$rollback_attempted rollback_verified=$rollback_verified error_exit=$error_exit backup_ref=$BACKUP_DIR readiness_sha256=$readiness_sha installer_sha256=$installer_sha prohibited_actions=host_reboot,vm_power_change,vm_reset,firewall_change,secret_read,database_write" \ >"$receipt_tmp" chmod 0640 "$receipt_tmp" if ! mv -n "$receipt_tmp" "$INSTALL_RECEIPT_PATH" \ @@ -527,7 +528,9 @@ static_source_preflight() { } post_install_static_gate() { + local blockers=() STATIC_POST_GATE_READY=0 + STATIC_POST_GATE_BLOCKERS="none" TARGET_HASHES_MATCH=0 UNIT_STATIC_VERIFY_READY=0 if cmp -s "$READINESS_SOURCE" "$READINESS_TARGET" \ @@ -540,6 +543,17 @@ post_install_static_gate() { UNIT_STATIC_VERIFY_READY=1 fi refresh_gate_state + [ "$TARGET_HASHES_MATCH" -eq 1 ] || blockers+=(target_hashes_mismatch) + [ "$UNIT_STATIC_VERIFY_READY" -eq 1 ] || blockers+=(unit_static_verify_failed) + [ "$TARGET_FILES_READY" -eq 1 ] || blockers+=(target_files_not_ready) + [ "$UNIT_CONTRACT_READY" -eq 1 ] || blockers+=(unit_contract_not_ready) + [ "$SUDO_POLICY_READY" -eq 1 ] || blockers+=(sudo_policy_not_ready) + [ "$AGENT99_99_DIRECT_ROUTE_READY" -eq 1 ] || blockers+=(agent99_direct_route_not_ready) + [ "$READINESS_NO_WRITE_READY" -eq 1 ] || blockers+=(readiness_no_write_not_ready) + [ "$TIMER_ENABLED" = "enabled" ] || blockers+=(timer_not_enabled) + if [ "${#blockers[@]}" -gt 0 ]; then + STATIC_POST_GATE_BLOCKERS="$(IFS=,; printf '%s' "${blockers[*]}")" + fi if [ "$TARGET_HASHES_MATCH" -eq 1 ] \ && [ "$UNIT_STATIC_VERIFY_READY" -eq 1 ] \ && [ "$TARGET_FILES_READY" -eq 1 ] \ @@ -552,6 +566,14 @@ post_install_static_gate() { fi echo "TARGET_HASHES_MATCH=$TARGET_HASHES_MATCH" echo "UNIT_STATIC_VERIFY_READY=$UNIT_STATIC_VERIFY_READY" + echo "TARGET_FILES_READY=$TARGET_FILES_READY" + echo "UNIT_CONTRACT_READY=$UNIT_CONTRACT_READY" + echo "SUDO_POLICY_READY=$SUDO_POLICY_READY" + echo "AGENT99_99_DIRECT_ROUTE_READY=$AGENT99_99_DIRECT_ROUTE_READY" + echo "READINESS_NO_WRITE_READY=$READINESS_NO_WRITE_READY" + echo "READINESS_CHECK_EXIT=$READINESS_CHECK_EXIT" + echo "TIMER_ENABLED=$TIMER_ENABLED" + echo "STATIC_POST_GATE_BLOCKERS=$STATIC_POST_GATE_BLOCKERS" echo "STATIC_POST_GATE_READY=$STATIC_POST_GATE_READY" [ "$STATIC_POST_GATE_READY" -eq 1 ] } diff --git a/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py b/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py index 7924d7c9e..0da6c077f 100644 --- a/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py +++ b/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py @@ -134,6 +134,30 @@ def test_install_receipt_does_not_claim_a_recovery_run() -> None: assert 'agent99_99_direct_runtime_probe=not_started' in receipt assert 'rollback_attempted=$rollback_attempted' in receipt assert 'rollback_verified=$rollback_verified' in receipt + assert 'static_post_gate_blockers=${STATIC_POST_GATE_BLOCKERS:-not_evaluated}' in receipt + + +def test_static_post_gate_reports_named_blockers_before_rollback() -> None: + text = source() + gate = text[ + text.index("post_install_static_gate() {") : text.index( + "wait_for_recovery_service_idle() {" + ) + ] + + for blocker in ( + "target_hashes_mismatch", + "unit_static_verify_failed", + "target_files_not_ready", + "unit_contract_not_ready", + "sudo_policy_not_ready", + "agent99_direct_route_not_ready", + "readiness_no_write_not_ready", + "timer_not_enabled", + ): + assert blocker in gate + assert 'STATIC_POST_GATE_BLOCKERS=' in gate + assert 'READINESS_CHECK_EXIT=' in gate def test_check_gate_requires_policy_route_timer_and_no_write_readback() -> None: