KB-406E

C2B-PRE-F1 — Sửa lỗi empty-staging và khôi phục fail-visible cho backup Directus mã hoá (2026-07-20)

32 min read Revision 1
current-statec2b-pre-f1directusbackup-encryptioncontrol-flow-repairempty-stagingfail-visiblepipefailerrexitterminal-finalizeratomic-deploymentisolated-test-matrixzero-touchgit-safetyrollback-readydisk-headroom-gatedr-status2026-07-20PASS_EXACT_NATURAL_BACKUP_CONTROL_FLOW_FAILURE_REPAIRED

C2B-PRE-F1 — Sửa lỗi empty-staging và khôi phục fail-visible cho backup Directus mã hoá (2026-07-20)

VERDICT = PASS_EXACT_NATURAL_BACKUP_CONTROL_FLOW_FAILURE_REPAIRED

MISSION_WINDOW      = 2026-07-20T03:49:36Z -> 2026-07-20T04:13Z  (UTC)
LIVE_SCRIPT_PRE     = 35f1095a8c70b91bc08a5d3a8f4a7284e58c7f222fe38ad009568056d3d922b0  (11,731 B)
LIVE_SCRIPT_POST    = 19e434320a26d5aae595c82e722c02e2880c9dee254bc52598ad074c8413288b  (16,123 B)
COMMIT              = 2ce85a63aaffc664225310c23b9b8788986fa4f3  (1 file, +88 -9, KHONG push)
NEXT_NATURAL_RUN_UTC= 2026-07-20T18:37:00Z
C2B1_ALLOWED        = NO  (van bi chan - xem §16)

⚠️ CẢNH BÁO ĐIỀU HÀNH ĐỌC TRƯỚC

Uptime Kuma sẽ chuyển DOWN lúc ~2026-07-20T05:10:46Z (khoảng 1 giờ sau khi báo cáo này được viết). Nếu Owner nhận cảnh báo Telegram "PG Backup GDrive" trong khoảng đó:

Đó là báo động trễ của lần chạy hỏng đêm 19/07, KHÔNG phải lỗi mới, và KHÔNG phải do bản vá này gây ra.

Cơ chế: monitor 14 có interval = 90000s (25h). Heartbeat cuối cùng nhận được là heartbeat synthetic msg=probe lúc 2026-07-19T04:10:46Z (đã bị loại khỏi bằng chứng thành công từ V3). 04:10:46Z + 25h = 05:10:46Z hôm nay. Đây chính là "≈9h32m hoãn báo động" mà V3 đã đo.

Bản vá không gửi heartbeat nào trong mission này. Heartbeat thật tiếp theo sẽ đến từ natural run 18:37:00Z.


1. Executive verdict

Lỗi control-flow chính xác đã làm lần chạy production tự nhiên đầu tiên chết sau ~1 giây đã được tái hiện, sửa, kiểm chứng cô lập, và triển khai nguyên tử. Ngoài ra defect thứ hai — đường thoát set -e không đi qua fail() khiến sự cố im lặng 8 giờ — cũng đã được đóng, đúng theo khuyến nghị (A)+(C) mà chính V3 đề xuất.

Tiêu chí nghiệm thu Kết quả
EMPTY_STAGING_FIRST_RUN PASS
EMPTY_STAGING_DELETE_COUNT 0
REAL_STAGING_ERROR_NOT_MASKED PASS
UNEXPECTED_SET_E_EXIT_REPORTED PASS
PIPELINE_FAILURE_REPORTED PASS
STATUS_TERMINAL_FAIL PASS
FAILED_STAGE_RECORDED PASS
LAST_SUCCESS_NOT_FALSELY_UPDATED PASS
LOCK_RELEASE PASS
CLEANUP PASS
SUCCESS_HEARTBEAT_ON_FAILURE NO
PLAINTEXT_FALLBACK NO
DISK_HEADROOM_GATE PASS (3.46x)
UNREPORTED_TERMINAL_EXIT_PATHS EXACT_BOUNDED_LIST (§6)

Không chạy production backup. Không upload, không xoá remote, không đụng credential/cron/GPG/IAM/monitor.


2. Nguồn đã đọc

Nguồn Ghi chú
knowledge/current-state/reports/claude-c2b-pre-encrypt-directus-database-backup-2026-07-18.md rev 1, 936 dòng, đọc toàn văn
knowledge/current-state/reports/claude-c2b-pre-v-prerun-gate-and-appendix-measurements-2026-07-19.md rev 1, đọc toàn văn
knowledge/current-state/reports/claude-c2b-pre-first-natural-production-run-verification-2026-07-20.md rev 1, đọc toàn văn
Live: /opt/incomex/scripts/backup-to-gdrive.sh 265 dòng, đọc toàn văn
Live: crontab -l (root), /etc/crontab đánh số dòng
Live: status/lock/log/staging, /root/c2bpre-work/{harness.sh,run-failtests.sh}

2.1 Đính chính so với giả định của mission

Mission §9 nói "các 7/7 scenario cũ phải được tái sử dụng". Đo thật cho thấy khác:

  • Ma trận cũ là S0–S6 (7 dòng), chuỗi ký tự 7/7 không tồn tại trong báo cáo gốc.
  • Ma trận cũ không có kịch bản pg_dump-failure, compression-failure, hay remote-verification-failure. Vậy T7, T8, T11 là mới, không phải "tái sử dụng".
  • Ma trận cũ chưa từng có kịch bản empty-staging. Đây chính là lý do defect lọt lưới: khối prune được thêm sau khi ma trận đã thiết kế xong, và chưa từng chạy với staging rỗng.
  • Khoá GPG dùng-một-lần của harness cũ đã bị xoá (§30 báo cáo gốc) nên phải sinh khoá throwaway mới cho S2.

3. Bằng chứng lỗi natural-run (đo lại, không tin memory)

NATURAL_RUN_ID   = 20260719T183701Z-2534894
STARTED_UTC      = 2026-07-19T18:37:01Z
DURATION         ~ 1 giay

Status file đóng băng — chính là dấu vân tay của việc không đi qua fail():

status=RUNNING          <- khong phai FAIL
stage=PRECHECK
last_success_utc=never
error_class=            <- RONG
payload=REDACTED

Log chỉ có ĐÚNG 1 dòng sau BACKUP START, rồi im lặng:

[2026-07-19 20:37:01 CEST] BACKUP START run_id=20260719T183701Z-2534894 ...
(het - khong co dong nao khac)

Staging RỖNG (total 8, chỉ ...), lock 0 byte, không tiến trình giữ lock.

3.1 Tái hiện có kiểm soát (chứng minh, không suy đoán)

Chạy khối gây lỗi nguyên văn trong /tmp/f1repro, bash 5.2.21 giống host:

Điều kiện Kết quả đo
staging RỖNG in ENTER, không in SURVIVED_PRUNE, rc=2
staging có 1 file mỗi loại in SURVIVED_PRUNE, rc=0

Và chứng minh vì sao ls không thể dùng được:

Lệnh Empty glob Lỗi thật
ls -1t <glob> rc=2 rc=2
find ... -name <pat> rc=0, output rỗng rc != 0

ls trả cùng một mã lỗi cho "không khớp gì" và "hỏng thật" nên về mặt ngữ nghĩa nó không thể biểu diễn "0 mục là thành công".

3.2 Deadlock tự khoá

Vòng prune chỉ sống sót nếu staging đã có ít nhất một .gpg; nhưng .gpg chỉ được tạo ở PG_DUMP/ENCRYPTsau điểm chết. Nên mỗi đêm sẽ chết y hệt, vĩnh viễn.

Trớ trêu đã ghi nhận: khối gây lỗi chính là "Lỗi 3 — tự phát hiện, sửa chủ động" của báo cáo gốc §23. Bản vá cho một bẫy pipefail lại rơi vào đúng bẫy pipefail. Đây là lần thứ 4 cùng lớp lỗi pipefail + errexit trong chuỗi mission này.


4. Git / config Safety Gate

Mục Giá trị
Repo /opt/incomex
Branch feat/s177-sprint1-round-a
HEAD PRE e2aa7cf0cc0b142d04aaf0fca2be693dd0a989fb
Live == git HEAD? — sha khớp, git diff HEAD rỗng
Dirty không liên quan 15 file M + nhiều ??không stage bất kỳ file nào trong số đó
Remote 0 remote nên push là bất khả thi về mặt cấu trúc
Backup byte-exact /root/c2b-f1-safety/20260720T040746Z/
Bundle PRE b4790bbb0d5e31192cacfc7bc29082f9e4bb623c98e3189ad438288044c6c315
git bundle verify PRE PASS — "records a complete history" (chạy từ trong repo)
Backup process đang chạy? KHÔNG
Lock có bị giữ? KHÔNG

4.1 Safe window

NOW_UTC                             = 2026-07-20T04:08:08Z
NEXT_NATURAL_RUN_UTC                = 2026-07-20T18:37:00Z
TIME_REMAINING_SECONDS              = 52,132  (14h 28m)
REQUIRED_SAFE_COMPLETION_BUFFER_SEC = 3,600
DEPLOYMENT_CUTOFF_UTC               = 2026-07-20T17:37:00Z
TIME_BEFORE_CUTOFF_SECONDS          = 48,532
SAFE_WINDOW                         = AMPLE_PASS

Lịch đọc live: crontab root dòng 54 37 20 * * * ...; CRON_TZ đầu tiên ở dòng 56 — SAU nên dòng 54 chạy theo TZ hệ thống Europe/Berlin (CEST +0200).

Bẫy đo đã tránh: date -u -d "today 20:37" parse đầu vào theo UTC nên SAI 2 giờ. Phải đi qua epoch. Kiểm chéo: 20:37 Berlin 2026-07-19 = 2026-07-19T18:37:00Z, khớp chính xác run_id đã quan sát.

4.2 Disk headroom gate

DISK_TOTAL_BYTES                 = 102,888,095,744  (95.82 GiB)
DISK_FREE_BYTES                  =  11,944,157,184  (11.12 GiB)
DISK_USED_PERCENT                = 89%
INODE_FREE                       = 11,912,634  (9% dung)
CURRENT_STAGING_FILE_COUNT       = 0
CURRENT_STAGING_BYTES            = 0

EXPECTED_NEXT_RUN_ARTIFACT_BYTES =     268,114,981   (db ~117 MB + cfg ~151 MB)
EXPECTED_PEAK_LOCAL_BYTES        =     498,114,981   (artifact + WORK_DIR truoc tar)
EXPECTED_STAGING_RETENTION_BYTES =     268,114,981   (LOCAL_KEEP=1)
WORST_CASE_RETENTION_BYTES       =     804,344,943   (PRECHECK giu 3/loai khi loi lien tiep)
SAFETY_RESERVE_BYTES             =   2,147,483,648
REQUIRED_TOTAL                   =   3,449,943,572
ESTIMATED_FREE_BYTES_AT_PEAK     =  11,446,042,203

DISK_HEADROOM_GATE = PASS   (margin 3.46x)

Cơ sở: DB ciphertext đo thật trước đây 120,505,714 B; proxy sống hôm nay là dump gz local 117,003,034 B (+~0.1% overhead GPG). CFG ~144 MiB là ước lượng chưa đo — kế thừa từ báo cáo gốc, đã đánh dấu rõ.


5. Bản đồ control-flow trước khi vá

entry -> set -Eeuo pipefail (L19) -> config -> mkdir staging/status (L48) -> log redirect (L51)
  -> flock (L54-58)  -- giu lock => exit 75 --> [CO Y ngoai moi bao cao]
  -> LAST_SUCCESS (L60) -> STAGE=PRECHECK (L62)
  -> status_write() / cleanup() / fail() (L63-95)
  -> trap cleanup EXIT (L96) ; trap fail INT TERM HUP (L97)
  -> status_write RUNNING PRECHECK (L99)   <-- tu day tro di moi loi PHAI hien thi
  -> precheck gpg/rclone/recipient/fpr/workdir (L103-109)
  -> * LOCAL-PRUNE (L114-119)  <=== DIEM CHET
  -> PG_DUMP (L125-138) -> COMPONENTS (L140-183) -> ENCRYPT (L185-194)
  -> VERIFY (L196-218) -> write_meta (L220-238) -> UPLOAD (L240-246)
  -> RETENTION (L248-255)  <-- cung construct, defect tiem an
  -> COMPLETE (L257-265) -> heartbeat up -> exit 0

6. Đường thoát không được báo cáo

UNREPORTED_TERMINAL_EXIT_PATHS = EXACT_BOUNDED_LIST

Trước khi vá — mọi lỗi từ L99 trở đi mà không đi qua || fail đều im lặng, gồm: set -e chết ở pipeline prune (L115 — đã xảy ra thật), set -u biến chưa gán, mọi exit lạc, và lỗi trong function/subshell.

Sau khi vá — chỉ còn một lớp duy nhất, bất khả kháng về mặt vật lý:

Đường Vì sao không thể báo cáo
L48 mkdir -p staging/status hỏng Chưa có thư mục status nên không thể ghi marker
L49 chmod hỏng như trên
L51 chuyển hướng log hỏng Chưa có log để ghi
L54 exec 9> lock hỏng Chưa có hạ tầng status
L57 exit 75 ALREADY_RUNNING CỐ Ý — xem dưới

exit 75 phải nằm ngoài finalizer: nếu không, lần gọi thứ hai sẽ ghi đè status của run đang giữ lock bằng một FAIL chưa từng xảy ra. Đây là hành vi đúng, không phải thiếu sót. Lớp "hạ tầng status hỏng" tương ứng S6 cũ và vẫn fail-closed (exit != 0).

Trả lời 7 câu hỏi bắt buộc của §6

  1. Empty staging biểu diễn thế nào để là zero-item success? find rc=0 + output rỗng nên mảng 0 phần tử nên vòng xoá không chạy lần nào.
  2. Phân biệt empty với unreadable/missing/error? find rc != 0 mới là lỗi. Đo thật: empty->rc=0, missing dir->rc=1. ⚠️ Cảnh báo trung thực: script chạy root qua cron, nên chmod 000 không tạo được lỗi (root bỏ qua permission — đo được rc=0 và vẫn liệt kê file). Vì vậy T4 dùng stub find exit 1 + missing-dir, không dùng permission.
  3. Cơ chế terminal failure có idempotent không? CÓ. FINALIZED guard + bash tự vô hiệu EXIT trap khi nó đang chạy. Đo: fail() -> finalize thấy đã báo -> không báo lại.
  4. Nguy cơ handler tự gọi lại/đệ quy? KHÔNG. ERR trap chỉ ghi LINENO, không exit, không gọi fail. finalize gỡ trap - EXIT ERR ngay dòng đầu.
  5. Success path bị failure trap ghi đè? KHÔNG. finalize chỉ báo FAIL khi rc != 0. Đo T12: status=PASS, failed_utc rỗng.
  6. Cleanup failure che mất original failure? KHÔNG. rc được chụp ở dòng đầu và exit "$rc" nguyên văn; set +e cục bộ để không lệnh báo cáo nào đổi được kết cục.
  7. Subshell/pipeline failure không được report? Đã đóng. Đo T6 với đúng construct gốc: status=FAIL, rc=2 giữ nguyên.

7. Thiết kế bản vá — và vì sao nó KHÔNG che lỗi thật

Hai thay đổi có giới hạn. Không dùng || true rộng, set +e rộng, hay nuốt stderr để làm test xanh.

7.1 find thay ls <glob> — sửa theo NGỮ NGHĨA

ls trả rc=2 cho cả hai trạng thái nên không thể phân biệt. find phân biệt được, nên lỗi thật vẫn chết — chỉ trạng thái rỗng thôi không còn bị nhầm là lỗi. Đây là mở rộng khả năng phát hiện lỗi, không phải thu hẹp.

Ba cạm bẫy bash đã đo và né:

Cạm bẫy Đo được Cách né
local x="$(cmd)" rc=0 (nuốt lỗi); tách ra -> rc=7 Khai báo local tách khỏi gán
f(){ ...; [ false ] && x=1; } hàm trả 1 nên giết script dưới set -e Dùng if tường minh
mapfile -t a <<<"" 1 phần tử ma nên xoá "" Bọc if [ -n "$listing" ]

7.2 Terminal finalizer trên EXIT

Chọn EXIT thay vì ERR trap sau khi đo: set -u unbound variable không kích hoạt ERR (T5b cho error_class=UNEXPECTED_EXIT_rc1 — không có hậu tố _line), nhưng kích hoạt EXIT. EXIT phủ nghiêm ngặt rộng hơn. ERR được giữ lại chỉ để ghi LINENO.

Dùng LINENO chứ không BASH_COMMAND: dòng lệnh có thể chứa secret, mà status file là artifact phải sạch.

7.3 Bảo toàn tuyệt đối

Không đổi: pg_dump options, gzip, GPG options/recipients, artifact contract, remote prefix, cron, retention count (PRECHECK giữ 3/loại, RETENTION giữ LOCAL_KEEP=1 — hai mốc khác nhau, cố ý, đã bảo toàn), Directus credentials, khối PIPESTATUS (đo: byte-identical).

shellcheck --severity=warning: live 2xSC2034 -> candidate 2xSC2034 nên IDENTICAL_SHELLCHECK_PROFILE, 0 finding mới.


8. Diff (đã kiểm redaction)

@@ STAGE="PRECHECK"
+FINALIZED=0
+ERR_LINE=0
+RETENTION_INPUT_COUNT=0
+RETENTION_DELETE_COUNT=0
 status_write() {
   local tmp; tmp="$(mktemp "${STATUS_DIR}/.status.XXXXXX")"
+  local failed_utc=""
+  if [ "$1" = "FAIL" ]; then failed_utc="$(date -u +%Y%m%dT%H%M%SZ)"; fi
     echo "last_success_utc=${LAST_SUCCESS}"
+    echo "failed_utc=${failed_utc}"

@@ fail()
+  FINALIZED=1

@@ traps
-trap 'cleanup' EXIT
+finalize() {
+  local rc=$?
+  trap - EXIT ERR
+  set +e                       # bao cao khong duoc phep doi ket cuc no dang bao
+  if [ "$FINALIZED" -eq 0 ] && [ "$rc" -ne 0 ]; then
+    FINALIZED=1
+    local cls="UNEXPECTED_EXIT_rc${rc}"
+    [ "$ERR_LINE" -ne 0 ] && cls="${cls}_line${ERR_LINE}"
+    status_write FAIL "${STAGE}" "$cls"
+    log "DIRECTUS_DB_BACKUP FAIL run_id=${RUN_ID} stage=${STAGE} error_class=${cls} payload=REDACTED"
+    curl ... "?status=down&msg=FAIL_stage%3D${STAGE}_class%3D${cls}" >/dev/null 2>&1
+  fi
+  cleanup
+  exit "$rc"
+}
+trap 'ERR_LINE=$LINENO' ERR
+trap 'finalize' EXIT
 trap 'fail "${STAGE}" SIGNAL_INTERRUPT' INT TERM HUP

@@ PRECHECK prune
+staging_list_newest_first() {
+  local pattern="$1" rc=0 out
+  out="$(find "$STAGING_DIR" -maxdepth 1 -type f -name "$pattern" -printf '%T@\t%p\n')" || rc=$?
+  [ "$rc" -eq 0 ] || return "$rc"
+  [ -n "$out" ] || return 0
+  printf '%s\n' "$out" | sort -rn | cut -f2-
+}
+staging_prune() {
+  local pattern="$1" keep="$2" stage="$3" rc=0 listing i
+  listing="$(staging_list_newest_first "$pattern")" || rc=$?
+  [ "$rc" -eq 0 ] || fail "$stage" "STAGING_ENUMERATION_FAILED_rc${rc}"
+  local -a items=()
+  if [ -n "$listing" ]; then mapfile -t items <<<"$listing"; fi
+  RETENTION_INPUT_COUNT=$(( RETENTION_INPUT_COUNT + ${#items[@]} ))
+  for (( i = keep; i < ${#items[@]}; i++ )); do
+    rm -f "${items[$i]}" "${items[$i]}.meta.json" || fail "$stage" STAGING_PRUNE_DELETE_FAILED
+    RETENTION_DELETE_COUNT=$(( RETENTION_DELETE_COUNT + 1 ))
+    log "  [${stage}][LOCAL-PRUNE] deleted=$(basename "${items[$i]}")"
+  done
+}
+RETENTION_INPUT_COUNT=0; RETENTION_DELETE_COUNT=0
 for _pat in 'incomex-prod-directus-db-*.sql.gz.gpg' 'incomex-prod-vps-config-*.tar.gz.gpg'; do
-  ls -1t ${STAGING_DIR}/${_pat} 2>/dev/null | tail -n +4 | while read -r _old; do
-    rm -f "$_old" "$_old.meta.json"
-    log "  [PRECHECK][LOCAL-PRUNE] deleted=$(basename "$_old")"
-  done
+  staging_prune "$_pat" 3 PRECHECK
 done
+log "  [PRECHECK][LOCAL-PRUNE] staging_input=${RETENTION_INPUT_COUNT} deleted=${RETENTION_DELETE_COUNT}"

@@ RETENTION  (cung defect, tiem an)
+RETENTION_INPUT_COUNT=0; RETENTION_DELETE_COUNT=0
 for pat in ...; do
-  ls -1t ${STAGING_DIR}/${pat} 2>/dev/null | tail -n +$((LOCAL_KEEP + 1)) | while read -r old; do
-    rm -f "$old" "$old.meta.json"; log "  [RETENTION][LOCAL] deleted=$(basename "$old")"
-  done
+  staging_prune "$pat" "$LOCAL_KEEP" RETENTION
 done
+log "  [RETENTION][LOCAL] staging_input=${RETENTION_INPUT_COUNT} deleted=${RETENTION_DELETE_COUNT}"

1 file changed, 88 insertions(+), 9 deletions(-). Thay đổi log nhãn duy nhất: [RETENTION][LOCAL] -> [RETENTION][LOCAL-PRUNE] cho từng file bị xoá (dòng tổng kết giữ nhãn [RETENTION][LOCAL]).


9. Chứng minh empty-staging

STAGING_RETENTION_INPUT_COUNT = 0
DELETE_OPERATION_COUNT        = 0
RETENTION_PRECHECK            = PASS
Test staging Kết quả
T1 rỗng hợp lệ 0 file exit=0 status=PASS stage=COMPLETE, staging_input=0 deleted=0
T2 1 file/loại 2 staging_input=2 deleted=0 (dưới ngưỡng giữ-3)
T3 vượt ngưỡng 10 PRECHECK staging_input=10 deleted=4 (giữ 3/loại, xoá 2 cũ nhất/loại) rồi RETENTION staging_input=8 deleted=6 (giữ 1/loại)

T3 xác nhận đúng file cũ nhất bị chọn, chỉ trong thư mục cô lập, không xoá rộng, và ngữ nghĩa đếm giữ nguyên so với tail -n +4 / tail -n +2.

Kiểm lại trên chính artifact ĐÃ TRIỂN KHAI (harness dựng từ live sau rename):

live_sha = 19e434320a26d5aae595c82e722c02e2...
exit_code = 0 · status = PASS · stage = COMPLETE · error_class = []
PRECHECK: staging_input=0 deleted=0
DEPLOYED_ARTIFACT_SURVIVES_EMPTY_STAGING = PASS

10. Chứng minh lỗi staging THẬT không bị che

T4 — stub find exit 1:

exit=1  status=FAIL  stage=PRECHECK  err=STAGING_ENUMERATION_FAILED_rc1
uploaded=0 (khong doi)  partials=0  workdirs=0  lock=FREE
RETENTION_PRECHECK = FAIL
TERMINAL_STATUS    = FAIL
FAILED_STAGE       = PRECHECK
EXIT_NONZERO       = YES

⚠️ Giới hạn đo trung thực: không test được bằng permission vì script chạy root; đo được find trên thư mục chmod 000 vẫn trả rc=0 và liệt kê file. Lỗi thật được bơm bằng stub + missing-dir, đúng bản chất "enumeration command thực sự lỗi".


11. Thiết kế terminal fail-visible

Đường thoát status stage error_class exit
fail() tường minh FAIL đúng stage phân loại rõ 1
set -e chết (T5) FAIL PRECHECK UNEXPECTED_EXIT_rc1_line142 1
set -u unbound (T5b) FAIL PRECHECK UNEXPECTED_EXIT_rc1 1
pipeline/pipefail (T6) FAIL PRECHECK UNEXPECTED_EXIT_rc2_line142 2 (giữ nguyên)
SIGTERM (S5) FAIL PG_DUMP SIGNAL_INTERRUPT 1

last_success không bị cập nhật giả — đo tường minh:

green run   -> last_success_utc = 20260720T040710Z
forced fail -> last_success_utc = 20260720T040710Z   (KHONG doi)
               failed_utc       = 20260720T040712Z   (DUOC ghi)
LAST_SUCCESS_NOT_FALSELY_UPDATED = PASS

Mọi kịch bản: lock=FREE sau khi thoát, partials=0, workdirs=0, stale_status_temps=0.


12. Ma trận test cô lập

Root cô lập /root/c2b-f1-work/harness-rootnằm ngoài production staging (chặt hơn harness cũ vốn đặt trong /var/lib/incomex/backup-staging/_harness). Fake remote thư mục local; KUMA_PUSH_URL=http://127.0.0.1:59999/disabled (cổng chết); pg_dump thay bằng /dev/urandom; letsencrypt bị vô hiệukhông copy cron-env.sh để không tạo bản sao plaintext của TLS key / mật khẩu Directus.

# Kịch bản exit status stage error_class upload
T1 Empty staging 0 PASS COMPLETE 4
T2 1 artifact/loại 0 PASS COMPLETE 4
T3 Vượt ngưỡng (5/loại) 0 PASS COMPLETE 4
T4 Enumeration lỗi thật 1 FAIL PRECHECK STAGING_ENUMERATION_FAILED_rc1 0
T5 set -e bất ngờ 1 FAIL PRECHECK UNEXPECTED_EXIT_rc1_line142 0
T5b set -u unbound 1 FAIL PRECHECK UNEXPECTED_EXIT_rc1 0
T6 Pipeline/subshell 2 FAIL PRECHECK UNEXPECTED_EXIT_rc2_line142 0
T7 pg_dump lỗi 1 FAIL PG_DUMP PG_DUMP_FAILED 0
T8 Compression lỗi 1 FAIL PG_DUMP PG_DUMP_FAILED ⚠️ 0
T9 GPG encrypt lỗi 1 FAIL ENCRYPT ENCRYPTION_FAILED 0
T10 Upload lỗi 1 FAIL UPLOAD UPLOAD_FAILED 0
T11 Remote-verify lỗi N/A không tồn tại gate
T12 Green end-to-end 0 PASS COMPLETE 4
S1 Thiếu recipient 1 FAIL PRECHECK RECIPIENT_MISSING 0
S2 Sai fingerprint 1 FAIL PRECHECK RECIPIENT_FINGERPRINT_MISMATCH 0
S5 SIGTERM 1 FAIL PG_DUMP SIGNAL_INTERRUPT 0
S6 Status infra hỏng 1 (không thể ghi marker) 0

Bất biến: 0 plaintext còn lại, 0 .partial, 0 workdir, 0 status temp, lock FREE, payload=REDACTED có mặt, 0 chuỗi giống secret, mọi kịch bản FAIL upload = 0.

⚠️ T8 — phát hiện TIỀN TỒN TẠI, không do bản vá

T8 kỳ vọng FORMAT_OR_COMPRESSION_FAILED nhưng nhận PG_DUMP_FAILED. Đo nguyên nhân:

PIPESTATUS = [141 1 0]

Khi gzip chết, nguồn phía trên nhận SIGPIPE (141). Thứ tự kiểm PS[2]->PS[0]->PS[1] khớp PS[0]=141 trước nên quy cho PG_DUMP. Đây là mặt đối xứng của "Lỗi 2" đã biết (khi gpg chết thì gzip bị SIGPIPE).

Chứng minh không do tôi: khối PG_DUMP/PIPESTATUS byte-identical giữa live và candidate. Vẫn fail-closed + fail-visible (FAIL, exit != 0, 0 upload); chỉ nhãn stage kém chính xác. Sửa nó đòi thay đổi thứ tự kiểm PIPESTATUSngoài phạm vi F1 (§7.D). Ghi vào backlog.

⚠️ T11 — không có gate để test

Script không có remote-verification gate. ENC_COUNT=$(rclone lsf ... || true) (dòng 338) là log sau-thành-công, và heartbeat cũng || true. Trùng backlog đã biết HEARTBEAT_SEMANTICS = AFTER_UPLOAD_WITHOUT_REMOTE_VERIFY. Không bịa PASS. Ngoài phạm vi F1.


13. Triển khai nguyên tử

1. cp candidate -> /opt/incomex/scripts/.backup-to-gdrive.sh.f1-candidate   (CUNG thu muc)
2. chown root:root, chmod 0755, shebang "#!/bin/bash", same_fs: dev 2049 == 2049
3. bash -n          -> OK
4. shellcheck       -> IDENTICAL_SHELLCHECK_PROFILE (0 finding moi)
5. guard: backup process = NONE, lock = NO
6. mv -f  (rename nguyen tu)
7. POST: bash -n OK, sha 19e4343...288b khop candidate, mode=755 root:root, size=16123
         cron target khong doi, 0 secret, 0 file tam sot

Không ghi từng phần lên live. Không thử-sai trên live: chỉ một lần rename duy nhất.


14. Production zero-touch

PRODUCTION_BACKUP_TRIGGER     = 0        (pgrep pattern dung runtime, khong tu khop)
PRODUCTION_UPLOAD             = 0
PRODUCTION_REMOTE_DELETE      = 0
HISTORICAL_ARTIFACT_CHANGE    = 0        (14 -> 14)
DIRECTUS_CREDENTIAL_CHANGE    = 0
CONTAINER_MUTATION_BY_MISSION = 0
SERVICE_RESTART_BY_MISSION    = 0
TELEGRAM_MONITOR_CHANGE       = 0
UPTIME_KUMA_PING              = 0
Mục PRE POST
legacy vps-backup-* 14 14
encrypted prefix 0 0
remote .partial 0 0
production staging file 0 0
status file frozen RUNNING không đổi
backup log 8,747,703 B 8,747,703 B
disk 89% 89%

Container — ID/StartedAt/RestartCount giống hệt PRE:

postgres         22540b78de8f  2026-04-17T05:35:18Z  restarts=0   healthy
incomex-directus 2245d86d2f6a  2026-05-08T09:13:06Z  restarts=0   healthy
incomex-qdrant   2b5c328f0b4d  2026-03-18T07:03:47Z  restarts=0   healthy
incomex-nuxt     42079c55bf9c  2026-07-19T12:15:26Z  restarts=79  healthy

incomex-nuxt restarts=79PRE và POST bằng nhau, không có restart trong cửa sổ mission. Đây là issue tự-restart đã biết; theo mission: ghi mốc, không điều tra, không quy cho bản vá (không lệnh nào của mission tác động tới container).

Vệ sinh test: ciphertext test + khoá GPG throwaway đã xoá (residual_ciphertext=0), giữ lại log/status làm bằng chứng.


15. Commit / bundle / rollback

COMMIT   = 2ce85a63aaffc664225310c23b9b8788986fa4f3
SUBJECT  = fix(backup): handle empty first-run staging and report terminal failures
FILES    = scripts/backup-to-gdrive.sh   (DUY NHAT, +88 -9)
STAGED   = 1 file  (KHONG git add -A, KHONG git add .)
--check  = CLEAN, secret scan = 0, 15 file dirty khac giu nguyen unstaged
PUSH     = KHONG  (repo co 0 remote => bat kha thi ve cau truc)

BUNDLE_PRE  = b4790bbb0d5e31192cacfc7bc29082f9e4bb623c98e3189ad438288044c6c315
BUNDLE_POST = cb6f77e92b7192003387f36d4ab05e9018427b01aa2cf93f811e019c196c21e3
verify      = PASS (ca hai, chay TU TRONG repo)

Rollback nguyên tử:

cp -p /root/c2b-f1-safety/20260720T040746Z/backup-to-gdrive.sh.PRE \
      /opt/incomex/scripts/.btg.rollback && \
chown root:root /opt/incomex/scripts/.btg.rollback && \
chmod 0755      /opt/incomex/scripts/.btg.rollback && \
mv -f /opt/incomex/scripts/.btg.rollback /opt/incomex/scripts/backup-to-gdrive.sh
# xac minh: sha256sum phai tra 35f1095a8c70b91bc08a5d3a8f4a7284e58c7f222fe38ad009568056d3d922b0
# (luu y: rollback = khoi phuc lai DUNG loi da sua; chi dung khi ban va gay hai ro ret)

16. Trạng thái DR hiện tại

PROVEN_RESTORABLE_OFFSITE_PRODUCTION_BACKUP   = NONE
LEGACY_PLAINTEXT_PRODUCTION_BACKUP_CANDIDATES = 14
LATEST_LEGACY_CANDIDATE_UTC                   = 2026-07-18T18:37:01Z
LATEST_LEGACY_CANDIDATE_AGE_DAYS              = 1.40
LEGACY_RESTORABILITY_STATUS = UNVERIFIED_NOT_ASSUMED_CORRUPT

Diễn giải bắt buộc — đọc cho đúng:

  • 14 artifact là phao production pg_dump/archive đang có thật (vps-backup-20260705... -> vps-backup-20260718..., tổng 3,822,611,835 B, liên tục không đứt).
  • Chúng chưa được decrypt/restore-verify trong mission này, vì chúng là plaintext chứa 13 static token sống; tải về sẽ tự tay tạo thêm một bản sao plaintext.
  • PROVEN = NONE KHÔNG đồng nghĩa 14 artifact chắc chắn hỏng. Nó chỉ nói chưa ai chứng minh.
  • Rủi ro thật: recovery point đang dừng ở bản 18/07già thêm 1 ngày mỗi ngày.

C2B1 vẫn BỊ CHẶN cho tới khi ENCRYPTED_BACKUP_RECOVERABILITY = PASS, và sau đó vẫn còn phải hoàn thành C2B1-R0 (kiểm kê consumer của admin credential).

Nhắc lại: NUXT_DIRECTUS_SERVICE_TOKEN chính là static token của admin@example.com nên xoay token sẽ giết 17 route. C2B1 = NO vẫn đúng.


17. Natural run kế tiếp

NEXT_NATURAL_RUN_UTC                 = 2026-07-20T18:37:00Z
TIME_UNTIL_NEXT_NATURAL_RUN          ~ 14h 24m   (tinh luc 2026-07-20T04:12:57Z)
CURRENT_PROVEN_RECOVERY_POINT_STATUS = NONE_PROVEN / LEGACY_UNVERIFIED_AGE_1.40_DAYS
MANUAL_TRIGGER_IN_F1                 = KHONG

Kỳ vọng nếu run 18:37Z thành công: encrypted prefix 0 -> 4 file (2 .gpg + 2 .meta.jsonkhông phải +1), status PASS/COMPLETE, log có DIRECTUS_DB_BACKUP PASS, Kuma nhận OK+encrypted&ping=<bytes> (không phải probe), legacy giữ nguyên 14, .partial=0.

Nếu run thất bại vì lý do khác: giờ đây nó sẽ nói rastatus=FAIL + failed_utc + failed_stage + error_class + heartbeat down, thay vì im lặng như đêm 19/07.


18. Hai lựa chọn trình Owner (KHÔNG tự chọn)

OPTION_A_WAIT_FOR_NEXT_NATURAL_RUN

Để nguyên, chờ 18:37:00Z hôm nay tự chạy và dùng nó làm bằng chứng production.

Phù hợp khi: natural run chỉ còn ~14h, production health đang PASS, disk margin 3.46x, Owner chấp nhận recovery gap hiện tại (1.40 ngày, tăng lên ~2.4 ngày trước khi có bằng chứng).

Ưu: sạch nhất, không cần quyền chạy tay, không thêm tải. Nhược: nếu run vẫn hỏng vì lý do khác, mất thêm một đêm — nhưng lần này sẽ có báo động.

OPTION_B_OWNER_GO_FOR_ONE_CONTROLLED_MANUAL_ENCRYPTED_RUN

Phải là mission riêng. Yêu cầu khái niệm: đúng một run, cửa sổ tải thấp, không plaintext, chỉ encrypted prefix, không chạy production retention delete, precheck resource-health, chứng minh decrypt/format, Owner GO rõ ràng, không dùng dịp đó để sửa thêm lỗi.

Ưu: rút ngắn DR window ~1 ngày và chứng minh recoverability sớm hơn. Nhược: đánh đổi bằng một lần chạy tay trên production.

Không chạy Option B trong mission này. Tôi không tự chọn.

Về C2B1-R0

Không tự chuyển sang C2B1-R0, dù nó read-only. Việc có chạy trong lúc chờ natural run hay không do Owner quyết sau khi đọc báo cáo này, căn cứ: verdict F1, disk headroom, thời gian còn lại, production health, tải của consumer survey, nguy cơ chồng natural backup. Nếu không đủ biên an toàn: ưu tiên để VPS yên và chờ natural run.


19. Đường dẫn bằng chứng

Nội dung Đường dẫn
Backup PRE + bundle /root/c2b-f1-safety/20260720T040746Z/
Script PRE byte-exact .../backup-to-gdrive.sh.PRE (11,731 B)
crontab PRE + cron target .../crontab-root.PRE, .../cron-target.PRE
Status PRE .../directus-db-backup.status.PRE
Bundle PRE/POST .../incomex-PRE.bundle, .../incomex-POST.bundle
Candidate + harness /root/c2b-f1-work/backup-to-gdrive.sh.candidate, .../harness.sh
Driver ma trận T1–T12 /root/c2b-f1-work/run-f1-tests.sh
Log bằng chứng test /root/c2b-f1-work/f1-harness-evidence.log
Status cuối của harness /root/c2b-f1-work/f1-final-status-evidence.txt
Live đã vá /opt/incomex/scripts/backup-to-gdrive.sh (16,123 B)

20. PAYLOAD=REDACTED

payload_redacted_refs_in_source     = 5
secret_patterns_in_deployed_source  = 0
secret_patterns_in_staged_diff      = 0
secretish_strings_in_status_file    = 0
BASH_COMMAND_written_to_status      = KHONG  (chi LINENO - co y)

Status file sau FAIL (bằng chứng thật, đã kiểm):

run_id=20260720T040712Z-3066904
last_success_utc=20260720T040710Z     <- KHONG bi day gia
failed_utc=20260720T040712Z           <- truong MOI
status=FAIL
stage=PRECHECK
error_class=UNEXPECTED_EXIT_rc1_line142
payload=REDACTED

Trường failed_utcthuần bổ sung: runbook chỉ cat file và grep status= / stage= / error_class= / last_success_utc; không có consumer nào parse theo vị trí. Đã kiểm: ngoài chính script, không script nào khác đọc file này.


21. Kết thúc bắt buộc

MANUAL_PRODUCTION_BACKUP_RUN  = NO      PRODUCTION_RETENTION_RUN     = NO
FULL_PRODUCTION_PG_DUMP       = NO      PRODUCTION_REMOTE_DELETE     = NO
PRODUCTION_UPLOAD             = NO      HISTORICAL_ARTIFACT_DELETE   = NO
PLAINTEXT_BACKUP_REENABLED    = NO      DIRECTUS_PASSWORD_CHANGE     = NO
DIRECTUS_STATIC_TOKEN_CHANGE  = NO      SECRET_MANAGER_MUTATION      = NO
IAM_MUTATION                  = NO      CRON_CHANGE                  = NO
GPG_KEY_CHANGE                = NO      TELEGRAM_TEST                = NO
TELEGRAM_MONITOR_CHANGE       = NO      UPTIME_KUMA_PING             = NO
CONTAINER_MUTATION_BY_MISSION = NO      SERVICE_RESTART_BY_MISSION   = NO
DIRECT_DB_WRITE               = NO      GITHUB_PUSH                  = NO
DOCKER_CLEANUP                = NO      C2B1_EXECUTED                = NO

TELEGRAM_DELIVERY_CHANNEL = CONFIRMED_FOR_EXISTING_VPS_ALERTSkhông suy thành DIRECTUS_BACKUP_FAILURE_ALERT_DELIVERY = CONFIRMED, vì lần hỏng vừa rồi không đi qua fail handler nên chưa từng có phép thử end-to-end. F1 chỉ bảo đảm mọi terminal failure đi tới status/log fail-visible path; việc chứng minh propagation tới Telegram để mission sau natural run.

Backlog mở (không sửa trong F1)

  1. T8 mis-attribution: gzip chết nên nguồn nhận SIGPIPE 141 nên báo PG_DUMP_FAILED thay vì FORMAT_OR_COMPRESSION_FAILED. Fail-closed vẫn đúng, chỉ sai nhãn.
  2. Không có remote-verification gate (HEARTBEAT_SEMANTICS = AFTER_UPLOAD_WITHOUT_REMOTE_VERIFY).
  3. 8 dump plaintext ~911 MB/opt/incomex/backups/pg/ (cron 02:27 độc lập, chứa 13 token sống).
  4. 14 artifact plaintext trên Drive — retention DORMANT, sẽ nằm đó vĩnh viễn.
  5. Kuma interval 25h nên báo động chậm tới ~1 ngày.

DỪNG sau báo cáo.

Back to Knowledge Hub knowledge/current-state/reports/claude-c2b-pre-f1-empty-staging-fail-visible-repair-2026-07-20.md