C2B-PRE-F1 — Sửa lỗi empty-staging và khôi phục fail-visible cho backup Directus mã hoá (2026-07-20)
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/7khô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ỉ . và ..), 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/ENCRYPT — sau đ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 + errexittrong 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? | CÓ — 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
- Empty staging biểu diễn thế nào để là zero-item success?
findrc=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. - Phân biệt empty với unreadable/missing/error?
findrc != 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ênchmod 000khô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 stubfindexit 1 + missing-dir, không dùng permission. - Cơ chế terminal failure có idempotent không? CÓ.
FINALIZEDguard + 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. - Nguy cơ handler tự gọi lại/đệ quy? KHÔNG. ERR trap chỉ ghi
LINENO, không exit, không gọifail.finalizegỡtrap - EXIT ERRngay dòng đầu. - Success path bị failure trap ghi đè? KHÔNG.
finalizechỉ báo FAIL khirc != 0. Đo T12:status=PASS,failed_utcrỗng. - Cleanup failure che mất original failure? KHÔNG.
rcđược chụp ở dòng đầu vàexit "$rc"nguyên văn;set +ecục bộ để không lệnh báo cáo nào đổi được kết cục. - Subshell/pipeline failure không được report? Đã đóng. Đo T6 với đúng construct gốc:
status=FAIL,rc=2giữ 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 có 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-root — nằ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ệu và khô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 PIPESTATUS — ngoà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=79 — PRE 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 = NONEKHÔ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/07 và già 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_TOKENchính là static token củaadmin@example.comnê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.json — khô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 ra — status=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_utc là thuầ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_ALERTS — khô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)
- T8 mis-attribution: gzip chết nên nguồn nhận SIGPIPE 141 nên báo
PG_DUMP_FAILEDthay vìFORMAT_OR_COMPRESSION_FAILED. Fail-closed vẫn đúng, chỉ sai nhãn. - Không có remote-verification gate (
HEARTBEAT_SEMANTICS = AFTER_UPLOAD_WITHOUT_REMOTE_VERIFY). - 8 dump plaintext ~911 MB ở
/opt/incomex/backups/pg/(cron 02:27 độc lập, chứa 13 token sống). - 14 artifact plaintext trên Drive — retention DORMANT, sẽ nằm đó vĩnh viễn.
- Kuma interval 25h nên báo động chậm tới ~1 ngày.
DỪNG sau báo cáo.