KB-5574

Claude — Balo Sync Button Deployment: Forensic & Recommendation (2026-07-17)

59 min read Revision 1
balonginxdeploymentforensicnuxtbridgecurrent-staterecommendation

KHẢO SÁT KIẾN TRÚC — ĐƯỜNG TRIỂN KHAI NÚT ĐỒNG BỘ BALO

Forensic & Recommendation

  • Ngày: 2026-07-17 (đo lúc 02:44Z–03:10Z)
  • Tác giả: Claude Opus 4.8 (1M) — kiến trúc sư điều tra độc lập
  • Loại: READ-ONLY forensic. 0 mutation.
  • Phạm vi: chấm dứt chuỗi HOLD 3D → 3D.1 → 3D.2 → 3D.3

1. EXECUTIVE VERDICT

VERDICT = READY_FOR_SINGLE_CONTROLLED_IMPLEMENTATION

Toàn bộ câu hỏi nền đã được trả lời bằng đo thật. Codex không phải đoán bất cứ điều gì khi thi công.

Ba phát hiện đảo ngược khung tư duy hiện tại:

(1) Chuỗi HOLD không phải do Nginx hay container. Nó do 3 điều kiện nền của production container mà chưa mission nào đo. Production incomex-nuxt không thể chạm tới bridge, vì 3 lý do độc lập, và cả 3 đều nằm trong docker-compose.yml, không phải trong Nginx:

# Thiếu Đo thật Hậu quả nếu deploy hôm nay
1 /run/incomex không được mount docker exec incomex-nuxt ls /run/incomexNo such file or directory 500 BALO_BRIDGE_FAILED_CLOSED
2 group_add rỗng GroupAdd=[]; container gid=65533(nogroup); socket gid=987 EACCES
3 NUXT_BALO_OPERATOR_ROLE_IDS chưa set `docker inspect ... grep -i baloNONE`

Điều kiện #3 chưa từng xuất hiện trong bất kỳ báo cáo 3A–3D.3 nào. Nếu lệnh kế tiếp chỉ làm Nginx route + cutover (đúng như 3D.3 đề xuất), kết quả sẽ là: route PASS, container healthy, và nút không hiện ra → HOLD thứ tư.

(2) HOLD của 3D.3 nằm trên một câu hỏi có ZERO liên kết runtime. 3D.3 dừng vì default.conf dirty (9+/26−). Đo thật: 25/35 dòng là comment; phần chức năng duy nhất là block /.well-known/ — và cả hai phía đều trả 404. Hơn nữa /opt/incomex không có remote, không script nào tự commit/push nó. Nên "commit baseline" là thao tác git thuần, file trên đĩa không đổi 1 byte, không reload Nginxkhông thể ảnh hưởng production. Đây là HOLD trên một câu hỏi sổ sách.

(3) Không cần canary. Route có thể tự chứng minh trước khi đụng Nuxt. Nginx route và Nuxt cutover độc lập nhau. Thêm route trỏ /api/balo/reconcile → nuxt_backend khi Nuxt production chưa có handler → Nuxt trả 404 của chính nó, khác hoàn toàn 404 của Agent Data. Tôi đã đo trước cả hai chữ ký:

TRƯỚC (Agent Data):  HTTP/1.1 404 Not Found        {"detail":"Not Found"}
SAU  (Nuxt cũ):      HTTP/1.1 404 Page Not Found   {"error":true,...,"statusMessage":"Page Not Found"}

Khác nhau ngay ở reason phrase. → Route được chứng minh không cần build, không cần canary, không cần cutover. Đây là chìa khoá phá chuỗi HOLD.

Điều kiện tiên quyết duy nhất (không phải blocker, nhưng phải biết): host đang 91% disk, cron disk-monitor.sh chạy mỗi giờdocker system prune -f — đã xoá đúng 2 container mà 3D.3 đi tìm. Mọi gate hoặc rollback dựa trên "container đã stop còn tồn tại" là bất khả thi trên máy này.


2. CURRENT-STATE SNAPSHOT

Phân tầng rõ, không trộn lớp.

2.1 SOURCE (git)

Repo HEAD Khớp báo cáo? Remote
/opt/incomex/dot de4730013ca3cee5f55cb54c00fe51f1c2f1068b (không kiểm — không liên quan)
/opt/incomex/docker/nuxt-repo 2ea41583422427b6b5e6d886fa7a754198e121b7 github.com/Huyen1974/web-test
/opt/incomex (root) b5e0f962600c4e42969df1e940c9c981254f0175 KHÔNG CÓ REMOTE
  • Root repo branch = feat/s177-sprint1-round-a, không phải main.
  • HEAD commit b5e0f96 = "nginx: route /api/reports/ to Nuxt"tiền lệ trực tiếp cho việc ta sắp làm.
  • Dirty: docker/nginx/conf.d/default.conf (M), docker/docker-compose.yml (M).
  • nuxt-repo dirty M web-rp-current = linked git worktree (không phải submodule, không .gitmodules), chứa untracked files. Không phải build input (nó là sibling của web/). Vô hại.

2.2 BUILT ARTIFACT

Artifact Path Đo
Production (live) /opt/incomex/deploys/nuxt-output nitro date 2026-07-11T02:49:53Z; grep balo/reconcilerỗng
Candidate (balo) /opt/incomex/deploys/nuxt-output-balo-20260716T234206Z 1713 files, 43M, content-fp 2a2de2f160e27062ca1e0ee09da67cbc1ea83d1971bac8db44a191efe4373d1f

Candidate byte-identical với web/.output trong repo (diff -rq → rỗng). Build 23:48:03Z, sau commit (15:22Z) 8h26m. find -newermt sau build → rỗng.

2.3 CONTAINER (runtime)

incomex-nuxt   890fee21392c   Created 2026-07-11T02:50:31Z   StartedAt 2026-07-16T23:57:35Z
               RestartCount=0  ExitCode=0  RestartPolicy=unless-stopped  healthy
               Mounts:  /opt/incomex/deploys/nuxt-output -> /app/.output (rw)
               GroupAdd: []        User: nuxtjs (uid=1001 gid=65533 nogroup)
               Env: KHÔNG có biến nào chứa "balo"
               Network: docker_incomex, alias [incomex-nuxt, nuxt], ip 172.18.0.6

StartedAtCreated → container bị stop rồi start lại, KHÔNG bị recreate. Nguyên nhân: cutover+rollback của 3D.2 (xem §4).

incomex-nginx  e50172b59a25   StartedAt 2026-06-30T00:44:14Z  RestartCount=0  Pid=1219625
               Mounts: /opt/incomex/docker/nginx/conf.d -> /etc/nginx/conf.d (READ-ONLY)

Master PID 1219625 không đổi kể từ 2026-06-30 → các reload (kể cả patch 07-15) giữ nguyên master. Nginx chưa từng restart trong toàn bộ saga.

2.4 RUNTIME CONFIG (Nginx effective)

host   sha256 = 3b947f4d0b61ccdf7dd194534fc39ba2721ef8a76d134d7d3785f9969cad1cc6
container sha = 3b947f4d0b61ccdf7dd194534fc39ba2721ef8a76d134d7d3785f9969cad1cc6   ← IDENTICAL
nginx -T      = phản ánh đúng file live (block /.well-known bare 404)              ← reload ĐÃ ăn
nginx -t      = syntax ok
grep -i balo trong nginx -T  →  0 hit

2.5 BRIDGE / SOCKET / DOT

socket   /run/incomex/balo-reconcile.sock   srw-rw----  root:incomex-balo-sync  mode=660
group    incomex-balo-sync:x:987:           ← GID 987 XÁC NHẬN, group KHÔNG có member
dir      /run/incomex  mode=750 root:incomex-balo-sync  ← cần GID 987 để traverse
units    incomex-balo-reconcile.socket (enabled, active) + .service (static, socket-activated)
         SocketGroup=incomex-balo-sync   SocketMode=0660   RemoveOnStop=true
         ExecStart=/opt/incomex/dot/bin/dot-balo-reconcile-bridge
         Hardened: ProtectSystem=strict, RestrictAddressFamilies=AF_UNIX
TCP listener = NONE

Chạy thật (được phép):

DRY_RUN|PASS|delta=0|write=0                    exit=0
VERIFY|PASS_RECONCILED|delta=0|write=0          exit=0
SQL_LOCK|manifest=PASS|sha256=c018f12c…|atomic_guard=PASS_NONEXECUTING

2.6 BALO DATA

BEGIN TRANSACTION READ ONLY; SELECT count(*) FROM balo_thuc_the;  →  2148
runner fingerprint: 2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1
                    count|min|max|md5(rows jsonb)|seq last_value|is_called|increment
TOTAL|2148|2148|0|0|0|0|0    delta=0    conflicts=0    sequence_safe=t

2.7 HOST

/dev/sda1  96G  87G  8.9G  91% /        ← 91% > THRESHOLD 85
cron: 0 * * * * /opt/incomex/scripts/disk-monitor.sh   → docker system prune -f  MỖI GIỜ
      435 lần cleanup đã ghi log; hiệu quả 91% → 91% (thu hồi ~0)

3. EVIDENCE / PROVENANCE TABLE

# Kết luận Loại Bằng chứng
1 3 HEAD khớp báo cáo FACT git rev-parse HEAD ×3
2 Nginx SSOT = host file, mount RO FACT docker inspect mount rw=false; host sha == container sha
3 Effective config == file live FACT nginx -T cho block /.well-known bare-404 (bản live), không phải JSON-404 (bản HEAD)
4 Không có route balo FACT nginx -T | grep -i balo → 0
5 /api/balo/reconcile hiện về Agent Data FACT curl{"detail":"Not Found"}, HTTP/1.1 404 Not Found
6 Nuxt 404 khác Agent Data 404 FACT curl /api/reports/definitely-not-a-routeHTTP/1.1 404 Page Not Found, {"error":true,...}
7 Diff nginx = 25 dòng comment + 1 block chức năng FACT diff sau khi strip comment: chỉ 1 hunk
8 Live nginx = bản patch OAuth 07-15 của tôi FACT sha 3b947f4d == sha ghi trong memory PHASE-2A; mtime 2026-07-15 11:50:37 +0200 (=09:50Z)
9 Bản pre-patch còn nguyên để restore FACT backups/nginx-ssot/20260715T094658Z/default.conf.LIVE-SNAPSHOT sha 9404dce8…
10 Root repo không remote, không auto-push FACT git remote -v rỗng; script push chỉ đụng nuxt-repo + agent-data-repo
11 Container bị xoá bởi cron prune FACT disk-monitor.log: Deleted Containers: b4fcfe38…, e512f557… @ 02:00:01 CEST
12 2 ID đó đúng là canary + candidate FACT khớp canary-create-id.txt / production-new-create-id.txt trong evidence 3D.2
13 incomex-nuxt restart = cutover+rollback 3D.2 FACT journal hasBeenManuallyStopped=true, exitStatus={0}; evidence rollback-old-start.txt
14 prod nuxt không mount /run/incomex FACT docker exec incomex-nuxt ls /run/incomex → No such file
15 prod nuxt GroupAdd=[] FACT docker inspect --format '{{.HostConfig.GroupAdd}}'[]
16 prod nuxt thiếu env balo FACT docker inspect … | grep -i balo → NONE
17 Nút gate trên config.public.baloOperatorRoleIds FACT BaloSyncButton.vue:19,69v-if="canSync"
18 Endpoint gate trên private key FACT baloReconcileContract.mjs authorizationGate
19 a40a1070… = Administrator, 1 active user FACT SELECT … FROM directus_rolesAdministrator; join users → Administrator | active | 1
20 Candidate == repo web/.output FACT diff -rq → rỗng; content-fp 2a2de2f1…
21 compose thiếu 10 env của agent-data FACT running=10 (PG_*×5, DIRECTUS_DB_*×5); compose khai=0
22 compose nuxt khớp runtime FACT mọi key compose có trong container; mem 512m == 512m
23 prune không đụng image có tag FACT prune -f chỉ xoá dangling; docker images -f dangling=true → rỗng; image prod có tag
24 Rebuild không thể byte-equal FACT buildId = UUID ngẫu nhiên + timestamp
25 package-lock.json pin nuxt 3.20.1 ≠ pnpm-lock 3.20.2 FACT bẫy npm ci

4. ROOT CAUSE OF REPEATED HOLDS

4.1 Nguyên nhân chung (một câu)

Mỗi mission kiểm chứng rất kỹ artifact của chính nó, nhưng chưa mission nào đo hợp đồng runtime của production container — nơi mọi thứ thực sự phải chạy.

Bốn blocker (3 cái container + 1 cái Nginx) đều lộ ra chỉ bằng hai lệnh read-only, chạy được từ trước Lệnh 3D:

docker inspect incomex-nuxt --format '{{.HostConfig.GroupAdd}} {{range .Mounts}}{{.Destination}} {{end}}'
docker inspect incomex-nuxt --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i balo
nginx -T | grep -c 'location.*api/balo'

Kết quả sẽ là [], /app/.output, (rỗng), 0lộ cả 4 blocker cùng lúc, trước khi viết một dòng code.

4.2 Bảng phân loại từng lần

Lần Triệu chứng Phân loại thật Đáng lẽ phải đo trước
3D SocketGroup=65533status=216/GROUP Giả định sai + false-green verification getent group 65533 (host) → rc=2. Một lệnh.
3D.2 /api/balo/reconcile → 404 Agent Data Thiếu khảo sát nền nginx -T | grep -c api/balo → 0. Một lệnh.
3D.3 container bị xoá + nginx dirty Trạng thái đổi ngoài mission + gate quá chặt không liên quan + SSOT drift tồn đọng crontab -l | grep prune; git diff (đọc nội dung, không chỉ đếm dòng)

Chi tiết 3D — không phải lỗi systemd. systemd không từ chối numeric GID về cú pháp. Nó fail vì GID 65533 không tồn tại trong group database của host (host nogroup=65534, container nogroup=65533). 3C chọn 65533 để container khỏi cần group_add. Ý tưởng đó vừa không chạy được, vừa sai về bảo mật (65533/nogroup là catch-all — mọi process unprivileged trong mọi container sẽ có quyền). systemd-analyze verify PASS vì nó là kiểm tĩnh cho một lỗi resolve lúc activation — phương pháp xác minh không có khả năng cấu trúc để bắt lỗi này. Đây là false green tốn trọn một mission.

Chi tiết 3D.2 — cái giá thật. Endpoint hoạt động đúng (401 trong container). Chỉ thiếu 1 dòng Nginx. Nhưng vì không đo route trước, 3D.2 đã: build đủ → canary → stop production nuxt 2 phút 16 giây (23:55:19 → 23:57:35) → phát hiện 404 → rollback. Production downtime thật, cho một sự thật lấy được bằng một lệnh grep read-only.

Chi tiết 3D.3 — HOLD trên câu hỏi không có mặt runtime. 3D.3 đúng khi từ chối suy đoán ai xoá container (event log đã hết). Nhưng nó nâng hai thứ không có khả năng ảnh hưởng production lên thành blocker:

  • "container tạm bị xoá" → provenance không nằm ở container. Nó nằm ở source commit + tree hash, cả hai còn nguyên. Container là runtime dùng-một-lần.
  • "nginx dirty 9+/26−" → 3D.3 báo con số mà không đọc nội dung. Đọc rồi thì: 25 dòng comment + 1 block cùng trả 404. Và commit baseline là git thuần, file không đổi.

4.3 Nguyên nhân điều hành (quan trọng nhất)

Mission được chia theo ARTIFACT (dot / nuxt / nginx / container), trong khi rủi ro nằm ở CHỖ GIAO NHAU giữa chúng.

Mỗi mission bị cấm đụng lớp bên cạnh, nhưng gate của nó lại đòi chắc chắn tuyệt đối về lớp bên cạnh đó. Kết quả tất yếu: mỗi mission chạy đến khi phát hiện một sự thật của lớp bên cạnh rồi HOLD. Đó chính là vòng lặp mà Owner mô tả — và nó là hệ quả cấu trúc của cách chia mission, không phải lỗi của agent nào.

Sửa: một mission "khảo sát bề mặt giao hàng" read-only trước mọi mission thi công — liệt kê hợp đồng runtime của container đích (mounts, groups, env) và bảng route hiệu lực của proxy. Báo cáo này chính là mission đó, làm muộn 4 lần.

4.4 Điều cần nói thẳng

3D.3 dừng lại là đúng theo chữ, nhưng gate của nó bất khả thi theo cấu trúc: trên host này, docker system prune -f chạy mỗi giờ và đĩa đã 91% > 85% vĩnh viễn. Mọi container đã stop đều chết trong ≤60 phút. Một gate đòi "container tạm còn tồn tại" không bao giờ có thể PASS. Nếu không phát hiện điều này, chuỗi HOLD sẽ lặp vô hạn: mission sau tái tạo container → prune xoá → mission sau nữa lại HOLD.


5. CANDIDATE REUSE / REBUILD DECISION

QUYẾT ĐỊNH = A. TÁI SỬ DỤNG NGUYÊN TRẠNG

Chuỗi provenance (mọi mắt xích FACT):

Mắt xích Bằng chứng
Candidate ≡ web/.output của repo diff -rqrỗng; cùng 1713 files; cùng content-fp 2a2de2f1…
Repo ở đúng HEAD 2ea4158…
Build tree sạch git status --porcelain -- web/ → rỗng
Worktree ≡ HEAD blob reconcile.post.ts sha HEAD == sha worktree (3423f16c…)
Build sau commit commit 15:22Z → build 23:48Z (+8h26m)
Không bị sửa sau build find -newermt "01:48:04" → rỗng
Không có secret bị bake baloOperatorRoleIds": ""; UUID admin không xuất hiện

Vì sao KHÔNG rebuild (B):

  1. Rebuild phá khả năng kiểm chứng. buildId = UUID ngẫu nhiên + wall-clock → {"id":"ff2db2ab-…","timestamp":1784245510604}. Bản build lại không bao giờ diff byte-equal với candidate → mất luôn cái neo so sánh.
  2. Bẫy version. web/package-lock.json (tracked, stale) pin nuxt 3.20.1; pnpm-lock.yaml (governing) pin 3.20.2 = đúng version trong artifact. npm ci sẽ âm thầm build sai version.
  3. Disk 91%. Build tại chỗ có rủi ro vận hành thật.

Khuyết điểm phải nói thẳng (INFERENCE, confidence cao nhưng không tuyệt đối): artifact không nhúng git SHA. nitro.json chỉ có "date". Sourcemap sourcesContent: falsekhông buộc được map↔blob bằng mật mã. Provenance ở đây là tái dựng từ filesystem + git state, không phải chứng thực mật mã. Với audit vận hành thiện chí: đủ tin. Với giả định có kẻ tấn công chủ động: không phải bằng chứng mật mã.

Việc 2 container bị xoá có ảnh hưởng provenance không?

KHÔNG. Provenance = source commit (2ea4158, đã có trên GitHub branch vps-daily-20260716)
                  + artifact tree (còn nguyên, tính lại được)
                  + DOT commit (de47300, có git bundle)
Container = runtime dùng-một-lần. Bằng chứng chúng tạo ra ĐÃ nằm trong evidence 3D.2.
Tái tạo chúng KHÔNG chứng minh thêm điều gì mà §8 không chứng minh rẻ hơn.

6. NGINX LIVE-VS-GIT DIFF FORENSIC

6.1 Câu hỏi 4 — SSOT thật là gì?

Git /opt/incomex (LOCAL-ONLY, no remote, branch feat/s177-sprint1-round-a)
      │  ← KHÔNG phải cơ chế deploy. Là sổ lịch sử.
      ▼
HOST  /opt/incomex/docker/nginx/conf.d/default.conf   sha 3b947f4d   ← ★ SSOT VẬN HÀNH
      │  bind mount READ-ONLY (rw=false)
      ▼
CONT  /etc/nginx/conf.d/default.conf                  sha 3b947f4d   ← IDENTICAL
      ▼
nginx -T effective                                    ← khớp file live

Đính chính quan trọng: tồn tại 4 bản default.conf trên máy. Chỉ một là SSOT.

Path sha256 mtime Vai trò
docker/nginx/conf.d/default.conf 3b947f4d… 2026-07-15 SSOT vận hành
deploys/infra-prev/nginx/conf.d/default.conf 9404dce8… 2026-07-11 bản deploy 11/07 (= pre-patch)
docker/nuxt-repo/infra/nginx/conf.d/default.conf d753fa0c… 2026-03-30 fork chết từ tháng 3 — KHÔNG phải SSOT
docker/nuxt-repo/web-rp-current/infra/nginx/conf.d/default.conf d753fa0c… 2026-06-05 bản sao của fork chết

⚠️ nuxt-repo/infra/nginx là repo ĐƯỢC AUTO-PUSH lên GitHub 2 lần/ngày. Nó chứa nginx từ tháng 3. Nếu ai đó "đồng bộ SSOT" theo hướng GitHub → VPS, họ sẽ đắp nginx tháng 3 lên production. Đây đúng là cơ chế đã gây sự cố 11/07. CẤM tuyệt đối coi bản này là SSOT.

6.2 Câu hỏi 5 — Vì sao dirty? (bảng theo yêu cầu)

Diff thô: 9 insertions(+), 26 deletions(-). Sau khi strip comment + dòng trắng: chỉ còn 1 hunk.

DIFF BLOCK PROVENANCE LIVE REQUIRED? CONFIDENCE RECOMMENDED TREATMENT
B1. Comment log_format gpt_mcp_sanitized bỏ chữ "restored 2026-05-23" Deploy 11/07 đắp artifact có comment generic. HEAD (b5e0f96, 07-08) là snapshot live giàu chú thích hơn. ❌ comment CAO — b5e0f96 commit đúng bản live 07-08; mtime live = 07-15 Nhận bản live
B2. 3 include MCP/KB: gộp comment, xoá dòng trắng. Directive y hệt như B1 ❌ comment CAO — diff chức năng = 0 Nhận bản live
B3. 4 include route MCP/KB: gộp comment. Directive y hệt như B1 ❌ comment CAO Nhận bản live
B4. location ~ ^/\.well-known/: HEAD = default_type application/json; return 404 '{...}' → LIVE = access_log off; return 404; Hai sự kiện chồng nhau: (a) deploy 11/07 revert fix S178-FIX9 (16/04) và đắp lại comment cũ "Well-known whitelist (S176)"; (b) patch KB-hang PHASE-2A của tôi 2026-07-15 xoá 3 block OAuth. sha 9404dce8…3b947f4d… khớp chính xác memory. mtime 09:50Z vs report 09:54Z. ⚠️ — nhưng cả hai đều trả 404 CAO GIỮ NGUYÊN BẢN LIVE. Đây là trạng thái hậu-sự-cố có chủ đích. Revert về HEAD = phải reload Nginx, rủi ro không đổi lấy lợi ích gì (body JSON thuần mỹ phẩm)
B5. Comment /ui-preview/, /ui-preview/mcp-writes/, /api/reports/ rút gọn như B1 ❌ comment CAO Nhận bản live

Kết luận §6: không còn khối nào "không rõ nguồn gốc". Toàn bộ diff truy được về hai sự kiện đã có hồ sơ: deploy 11/07 và patch OAuth 07-15. Nỗi lo "gom thay đổi không rõ nguồn" của Phương án A đã tan.

6.3 Câu hỏi 6 — Có nên commit baseline?

ĐỀ XUẤT = A (nhận toàn bộ live default.conf làm baseline), thực thi thành MỘT COMMIT RIÊNG
          ngay trước commit route, TRONG CÙNG mission.

Luận cứ quyết định — thao tác này có ZERO mặt runtime:

git add docker/nginx/conf.d/default.conf && git commit
   → file trên đĩa: KHÔNG đổi 1 byte (vẫn 3b947f4d)
   → container:     KHÔNG đọc lại (mount RO, không reload)
   → nginx:         KHÔNG reload
   → GitHub:        KHÔNG THỂ push (repo không remote; không script nào đụng root repo)
⇒ Về mặt vật lý, commit baseline KHÔNG THỂ ảnh hưởng production.
Tiêu chí A. Commit live làm baseline B. Reconcile từng thay đổi rồi mới thêm route C. Không commit; hash-lock + patch
An toàn production Zero — git thuần ❌ Phải sửa file live + reload để "sửa" comment ✅ Zero
Rollback git revert ✅ nhưng nhiều bước ✅ file backup
SSOT consistency ✅ Git == live Vĩnh viễn drift
Phạm vi 1 commit, 1 file Nhiều vòng, churn comment vô nghĩa 0 commit
Rủi ro gom thay đổi không rõ nguồn Đã tan (§6.2 truy hết)
Rủi ro để live ≠ Git ✅ Đóng ✅ Đóng Mission sau HOLD y hệt 3D.3
Tiền lệ b5e0f96 làm đúng thế này

Bác B: B đòi "sửa live cho khớp Git" — nghĩa là sửa 25 dòng comment và revert block /.well-known/, tức đụng file live + reload Nginx, để đổi lấy... một body JSON. B rủi ro hơn A trong khi A rủi ro bằng 0. Và block đó là trạng thái hậu-sự-cố có chủ đích — không được revert tuỳ tiện.

Bác C: C là lựa chọn đã ngầm áp dụng suốt từ 07-11 và chính nó sinh ra HOLD 3D.3. Chọn C = bảo đảm mission sau lại HOLD.

Lưu ý B5 trong memory ("CẤM đồng bộ VPS↔GitHub tới khi deploy hardening PASS") KHÔNG mâu thuẫn với A: lệnh cấm đó nhắm vào GitHub → VPS qua 2 deploy workflow (đã disable) và vào nuxt-repo/infra/nginx. Commit A nằm ở root repo local-only, không remote, không script nào push. Không có đường lan.

⚠️ Không kiểm được (UNKNOWN, §13): gh trên VPS chưa auth → không xác minh được 2 workflow (web-test Deploy-to-VPS 233946923, agent-data Vector-Audit 244569412) hiện còn disabled hay không. Chúng đã bị disable ngày 07-15. Cần Owner kiểm từ máy có auth.


7. ROUTE ARCHITECTURE OPTIONS (Câu hỏi 7)

7.1 Sự thật kỹ thuật phải đính chính trước

Comment trong config nói "Must be BEFORE the /api/ catch-all". Với prefix location, điều đó không đúng — Nginx chọn prefix DÀI NHẤT, không theo thứ tự file. Thứ tự chỉ quyết định với regex. Regex /api duy nhất là:

location ~ ^/api/(chat|kb/list|kb/get|ingest)(/.*)?$   # KHÔNG match balo

Bảng route hiệu lực trong server vps.incomexsaigoncorp.vn (:443):

= /api/openapi.json         → static           ← tiền lệ EXACT ĐÃ TỒN TẠI
  /api/directus|knowledge|workflows|transitive|discovery|registries-pivot|
  reports|registry|registries/                → nuxt_backend
~ ^/api/(chat|kb/…|ingest)  → (mcp/kb)
  /api/                     → agent_data_backend   [rewrite ^/api/(.*) /$1 break]

7.2 So sánh 4 phương án

A. location = /api/balo/reconcile B. location /api/balo/ C. Đổi endpoint sang prefix đã route D. Namespace /api/ thống nhất mới
Thay đổi Nginx +8 dòng, 1 block +8 dòng, 1 block 0 Tái cấu trúc lớn
Thay đổi Nuxt 0 0 Phải sửa source + REBUILD (mất §5) Lớn
Độ rộng route 1 path chính xác cả /api/balo/* rất rộng
Va chạm Agent Data 0 (exact thắng tuyệt đối) 0 (prefix dài hơn) 0 Cao
Regex interaction Không có= short-circuit, Nginx bỏ qua regex hoàn toàn có đánh giá regex (không match)
Mở rộng tương lai cần thêm block/endpoint ✅ tự nhận endpoint mới
Auth/CSRF Không đổi — gate ở Nuxt như A như A như A
Rollback Xoá 1 block + reload như A rebuild khó
Đơn giản cao nhất cao thấp thấp
ĐỀ XUẤT = A. location = /api/balo/reconcile → nuxt_backend

Vì sao A, không B: A là bề mặt hẹp nhất có thể — đúng một path. Exact match = khiến Nginx dừng ngay, không đánh giá regex → chứng minh được không tương tác với ^/api/(chat|kb/…). Hệ thống hiện chỉ có một endpoint balo; B mở sẵn cả namespace cho một nhu cầu chưa tồn tại. Tiền lệ exact đã có: location = /api/openapi.json. Đường nâng cấp: khi thật sự có endpoint balo thứ hai → đổi sang location ^~ /api/balo/.

Bác C, D vì cả hai buộc rebuild Nuxt → mất artifact đã kiểm chứng (§5) và kéo theo bẫy nuxt 3.20.1.

Không đề xuất browser gọi thẳng bridge. Không mở bridge ra TCP. (Bridge RestrictAddressFamilies=AF_UNIX, TCP_LISTENER=NONE — giữ nguyên.)

7.3 Khối chính xác + điểm chèn

Chèn vào /opt/incomex/docker/nginx/conf.d/default.conf sau dòng 242 (đóng block /api/reports/), trước dòng 244 (location /api/registry/):

    # Balo sync gateway (nút "Đồng bộ Balo từ PG"). Exact match: bề mặt hẹp nhất,
    # short-circuit toàn bộ regex, không đụng /api/ catch-all của Agent Data.
    location = /api/balo/reconcile {
        proxy_pass http://nuxt_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

KHÔNG thêm rewrite (catch-all /api/rewrite ^/api/(.*) /$1 break — Nuxt cần nguyên path). OriginX-Incomex-Action đi qua mặc định (Nginx chỉ chặn header bị unset tường minh) → gate CSRF nguyên vẹn.


8. DEPLOYMENT / CANARY OPTIONS (Câu hỏi 8)

8.1 Cách rẻ nhất: ROUTE TRƯỚC, CUTOVER SAU

Nhận thức then chốt: route và cutover độc lập, và route tự chứng minh được với Nuxt production hiện tại.

Thêm route khi Nuxt cũ chưa có handler → request tới Nuxt → Nuxt trả 404 của nó. Chữ ký khác hoàn toàn — đã đo trước:

HTTP status line Body
Trước (Agent Data) HTTP/1.1 404 Not Found {"detail":"Not Found"}
Sau (Nuxt cũ, route đã live) HTTP/1.1 404 Page Not Found {"error":true,"url":"…","statusCode":404,"statusMessage":"Page Not Found"}

Vì sao an toàn tuyệt đối: /api/balo/reconcile hiện đang 404. Không chức năng nào dùng nó. Đổi 404-của-agent-data thành 404-của-nuxt không thể làm hỏng gì.

8.2 So sánh các phương án kiểm

Phương án Mutation Chứng minh được gì Rủi ro Đánh giá
★ Route-first (§8.1) 1 file + reload Route hiệu lực, không cần Nuxt ~0 CHỌN
Canary container tạo container Nội dung + auth + socket prune xoá trong ≤60′; từng làm prod down 2′16″
Canary + network alias riêng container + network như trên Chia sẻ alias nuxtround-robin DNS nuốt traffic thật Nguy hiểm
Route tạm theo host/header nginx + reload như route-first nhưng phức tạp hơn Config tạm sót lại
Test container trực tiếp exec Chỉ nội bộ — đúng cái 3D.2 đã PASS rồi vẫn hỏng ❌ Không đủ

Không dựng framework canary mới. Bằng chứng canary mà 3D.2 cần đã có trong evidence của nó (gates 401/403/403/400/404 PASS). Cái nó thiếu là route — và route giờ được chứng minh trực tiếp, rẻ hơn.

8.3 Câu hỏi 9 — Gate nào là invariant thật?

Điều kiện Phân loại Lý do
Source commit 2ea4158 / de47300 INVARIANT THẬT Bất biến, có trên GitHub + bundle
Artifact tree 2a2de2f1… INVARIANT THẬT Tính lại được bất cứ lúc nào
Balo fingerprint 8ed1cd5d… INVARIANT THẬT Bảo vệ dữ liệu — gate quan trọng nhất
nginx sha trước/sau + nginx -t INVARIANT THẬT
Backup artifact + compose INVARIANT THẬT File — prune không đụng
Rollback image (có tag) INVARIANT THẬT prune -f chỉ xoá dangling; image prod có tag, đang dùng
Canary gates (401/403/400/404) 🔶 Evidence tái tạo được Đã PASS ở 3D.2; sẽ đo lại qua public route
Container healthcheck 🔶 Evidence tái tạo được
Container canary stopped còn tồn tại TRẠNG THÁI TẠM — CẤM làm gate Bất khả thi: prune mỗi giờ, disk 91% > 85% vĩnh viễn
Container failed stopped còn tồn tại TRẠNG THÁI TẠM — CẤM làm gate như trên

Kết luận Câu 9: KHÔNG. Deployment không được phụ thuộc container cũ/canary còn tồn tại. Trên host này đó là gate không bao giờ PASS được. Rollback phải dựa hoàn toàn vào file + image có tag.


9.1 CURRENT STATE (đứt ở 4 chỗ)

Browser (Administrator)
  │  POST /api/balo/reconcile
  ▼
┌──────────────────────────────────────── HOST ────────────────────────────────────────┐
│  Nginx [container incomex-nginx]                                                     │
│    location /api/  → agent_data_backend        ✗ ĐỨT #1: không có route balo         │
│         └──────────────► Agent Data → 404 {"detail":"Not Found"}                     │
│                                                                                      │
│  Nuxt [container incomex-nuxt]  uid=1001 gid=65533  GroupAdd=[]                      │
│    mount: deploys/nuxt-output → /app/.output   ✗ ĐỨT #2: artifact CŨ, không có balo  │
│    env:   (không có NUXT_BALO_*)               ✗ ĐỨT #3: → 503, NÚT VÔ HÌNH          │
│    /run/incomex                                ✗ ĐỨT #4: KHÔNG MOUNT → 500           │
│                                                                                      │
│  ────────────── phần dưới đây ĐÃ SỐNG 100% (3D.1 PASS, tôi đo lại PASS) ──────────── │
│  systemd socket  /run/incomex/balo-reconcile.sock  root:incomex-balo-sync 0660 gid=987│
│    └► dot-balo-reconcile-bridge (root, AF_UNIX only, socket-activated)               │
│         └► flock -n /run/lock/incomex-balo-reconcile-sync.lock  (singleton)          │
│              └► dot-balo-reconcile --sync                                            │
│                   └► dot-pg-atomic-apply  (SQL SHA-locked)  ★ NƠI GHI DUY NHẤT       │
│                        └► PostgreSQL: balo_thuc_the (2148)                           │
└──────────────────────────────────────────────────────────────────────────────────────┘

9.2 TARGET STATE

Browser (Administrator, session Directus, same-origin)
  │  POST /api/balo/reconcile
  │  Origin: https://vps.incomexsaigoncorp.vn   X-Incomex-Action: balo-reconcile-v1   body: ∅
  ▼
╔══════════════════════════════════ HOST (bare metal) ═══════════════════════════════╗
║ ┌── container incomex-nginx ────────────────────────────────────────────────────┐  ║
║ │  location = /api/balo/reconcile → proxy_pass http://nuxt_backend   ◄── SỬA #1 │  ║
║ │  (exact match: short-circuit regex, không đụng /api/ catch-all)               │  ║
║ │  mount: docker/nginx/conf.d → /etc/nginx/conf.d  (READ-ONLY)                  │  ║
║ └───────────────────────────────┬───────────────────────────────────────────────┘  ║
║                                 │ upstream nuxt_backend → nuxt:3000 (docker_incomex)║
║ ┌── container incomex-nuxt ─────▼───────────────────────────────────────────────┐  ║
║ │  uid=1001(nuxtjs)  gid=65533  group_add: ["987"]                    ◄── SỬA #3 │  ║
║ │  mount: deploys/nuxt-output → /app/.output   (artifact balo)        ◄── SỬA #2 │  ║
║ │  mount: /run/incomex → /run/incomex                                 ◄── SỬA #4 │  ║
║ │  env:   NUXT_BALO_OPERATOR_ROLE_IDS=a40a1070-…      (endpoint auth) ◄── SỬA #5 │  ║
║ │         NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS=a40a1070-…  (nút hiện)   ◄── SỬA #6 │  ║
║ │                                                                               │  ║
║ │  server/api/balo/reconcile.post.ts                                            │  ║
║ │    requestGate     → 405/400/403/403/403   (method, body∅, Origin,            │  ║
║ │                                             Sec-Fetch-Site, X-Incomex-Action) │  ║
║ │    fetchCurrentDirectusUser ──────────► directus:8055  ★ NƠI AUTH DIỄN RA     │  ║
║ │    authorizationGate → 503/401/403         (allowlist role rỗng ⇒ fail-closed)│  ║
║ │    callBaloBridge(config.baloReconcileSocketPath)  ← path từ config, KHÔNG    │  ║
║ │                                                      BAO GIỜ nhận từ request  │  ║
║ └───────────────────────────────┬───────────────────────────────────────────────┘  ║
║                                 │ AF_UNIX  "BALO_SYNC_V1\n"  (125s, cap 16KB)      ║
║ ┌───────────────────────────────▼── HOST process (không container) ─────────────┐  ║
║ │  systemd .socket → /run/incomex/balo-reconcile.sock  root:incomex-balo-sync   │  ║
║ │                    mode 0660, gid 987   (dir /run/incomex mode 0750)          │  ║
║ │  systemd .service → dot-balo-reconcile-bridge (root, ProtectSystem=strict,    │  ║
║ │                     RestrictAddressFamilies=AF_UNIX, socket-activated)        │  ║
║ │    argv cố định = [dot-balo-reconcile, --sync]   shell=false                  │  ║
║ │      └► flock -n /run/lock/incomex-balo-reconcile-sync.lock  ★ NƠI LOCK       │  ║
║ │           (đang chạy ⇒ ALREADY_RUNNING, rc=75 ⇒ HTTP 409)                     │  ║
║ │           └► dot-balo-reconcile --sync:  dry-run → apply → verify             │  ║
║ │                └► dot-pg-atomic-apply  (SQL SHA-locked vs manifest            │  ║
║ │                     c018f12c…, atomic guard PASS_NONEXECUTING)                │  ║
║ │                     ★★ NƠI WRITE DUY NHẤT TRONG TOÀN HỆ THỐNG ★★              │  ║
║ └───────────────────────────────┬───────────────────────────────────────────────┘  ║
║ ┌───────────────────────────────▼── container postgres ────────────────────────┐   ║
║ │  balo_thuc_the   2148 rows   fp 8ed1cd5d2f93f5f8a970497bd5e4ec87              │   ║
║ └──────────────────────────────────────────────────────────────────────────────┘   ║
╚════════════════════════════════════════════════════════════════════════════════════╝
   ↩ BALO_SYNC_RESULT|status=…|before_total=…|after_total=…|verified=…
      status ∈ {UPDATED, ALREADY_UP_TO_DATE, HOLD, FAILED, ALREADY_RUNNING}

Nút tự xuất hiện: DirectusTable.vue<BaloSyncButton v-if="resolvedCollection === 'balo_thuc_the'" @synced="handleRefresh" /> → tự áp cho 4 report hiện tại và mọi report Balo tương lai, không hardcode URL/report ID. (3C: BALO_UI_INHERITANCE|PASS|current=4|future=1|non_balo_hidden=1.)


Một mission duy nhất, 6 pha. Không chia nhỏ thêm — mọi câu hỏi nền đã đóng.

PHA 0 — PREFLIGHT (READ-ONLY, không gate nào dựa trên container tạm)

git -C /opt/incomex/dot rev-parse HEAD                  # == de4730013ca3cee5f55cb54c00fe51f1c2f1068b
git -C /opt/incomex/docker/nuxt-repo rev-parse HEAD     # == 2ea41583422427b6b5e6d886fa7a754198e121b7
git -C /opt/incomex rev-parse HEAD                      # == b5e0f962600c4e42969df1e940c9c981254f0175
sha256sum /opt/incomex/docker/nginx/conf.d/default.conf # == 3b947f4d0b61ccdf7dd194534fc39ba2721ef8a76d134d7d3785f9969cad1cc6
getent group incomex-balo-sync                          # == incomex-balo-sync:x:987:   ★ nếu GID ≠ 987 → DỪNG
stat -c '%a %U:%G' /run/incomex/balo-reconcile.sock     # == 660 root:incomex-balo-sync
/opt/incomex/dot/bin/dot-balo-reconcile --dry-run       # == DRY_RUN|PASS|delta=0|write=0
# fingerprint TRƯỚC:  2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1
cd /opt/incomex/deploys/nuxt-output-balo-20260716T234206Z && \
  find . -type f -exec sha256sum {} \; | sort -k2 | sha256sum   # == 2a2de2f160e27062ca1e0ee09da67cbc1ea83d1971bac8db44a191efe4373d1f
df -h /                                                 # ghi nhận; cần ≥ ~1G trống cho backup 43M

PHA 1 — GIT BASELINE ⟨MUTATION: git-only, ZERO runtime⟩

git -C /opt/incomex add docker/nginx/conf.d/default.conf docker/docker-compose.yml
git -C /opt/incomex commit -m "baseline: adopt live nginx + compose as SSOT

nginx default.conf: live 3b947f4d. Diff vs HEAD b5e0f96 = 25 dòng comment
(deploy 11/07 đắp comment generic) + 1 block /.well-known/ (patch KB-hang
PHASE-2A 2026-07-15, 9404dce8 -> 3b947f4d). Cả hai phía đều trả 404.
compose: bản human-authored đang chạy.
File trên đĩa KHÔNG đổi; không reload; repo local-only, không remote."

PROOF bắt buộc: sha256sum default.conf **trước == sau == 3b947f4d; nginx **không** reload; docker ps` không đổi.

PHA 2 — NGINX ROUTE ⟨MUTATION #1 — tự chứng minh, KHÔNG cần Nuxt⟩

TS=$(date -u +%Y%m%dT%H%M%SZ); B=/opt/incomex/backups/nginx-ssot/$TS; mkdir -p $B
cp -a /opt/incomex/docker/nginx/conf.d/default.conf $B/default.conf.PRE   # sha 3b947f4d
docker exec incomex-nginx nginx -T > $B/nginx-T.PRE.txt
# chèn block §7.3 sau dòng 242, trước dòng 244
docker exec incomex-nginx nginx -t                    # phải: syntax is ok / test is successful
docker exec incomex-nginx nginx -s reload

TEST QUYẾT ĐỊNH (không đụng Nuxt):

curl -sS -D- https://vps.incomexsaigoncorp.vn/api/balo/reconcile
#  PHẢI:  HTTP/1.1 404 Page Not Found   {"error":true,...,"statusMessage":"Page Not Found"}
#  CẤM:   HTTP/1.1 404 Not Found        {"detail":"Not Found"}      ← vẫn là Agent Data ⇒ ROLLBACK
# không hồi quy:
curl -s -o /dev/null -w '%{http_code}\n' https://vps.incomexsaigoncorp.vn/api/reports/live   # 400 (như trước)
curl -s -o /dev/null -w '%{http_code}\n' https://vps.incomexsaigoncorp.vn/                    # 200
docker exec incomex-nginx nginx -T | grep -c 'location = /api/balo/reconcile'                 # 1
# master PID phải KHÔNG đổi (reload, không restart):
docker inspect incomex-nginx --format '{{.State.Pid}}'                                        # 1219625

Rollback pha 2: cp $B/default.conf.PRE → default.conf; nginx -t; nginx -s reload (≈5s).

PHA 3 — COMMIT ROUTE ⟨git-only⟩

git -C /opt/incomex add docker/nginx/conf.d/default.conf
git -C /opt/incomex commit -m "nginx: exact route /api/balo/reconcile -> Nuxt (Balo sync button)

Mirror b5e0f96 (/api/reports/). Exact match: bề mặt hẹp nhất, short-circuit
regex, không đụng /api/ Agent Data catch-all. Local-only, không push."

PHA 4 — NUXT CUTOVER ⟨MUTATION #2 — nơi rủi ro thật sự nằm⟩

4a. Backup (prune-proof — chỉ file):

TS=$(date -u +%Y%m%dT%H%M%SZ)
cp -a /opt/incomex/docker/docker-compose.yml /opt/incomex/backups/compose.pre-balo-$TS.yml
mv /opt/incomex/deploys/nuxt-output /opt/incomex/deploys/nuxt-output.bak.pre-balo-$TS
cp -a /opt/incomex/deploys/nuxt-output-balo-20260716T234206Z /opt/incomex/deploys/nuxt-output
# GIỮ candidate gốc làm provenance — KHÔNG mv nó
docker inspect incomex-nuxt --format '{{.Image}}'   # ghi image rollback (có tag ⇒ prune không đụng)

4b. Sửa compose — service nuxt (chỉ service này):

    environment:
      # … giữ nguyên toàn bộ key hiện có …
      NUXT_BALO_OPERATOR_ROLE_IDS: a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac
      NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS: a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac
    volumes:
      - /opt/incomex/deploys/nuxt-output:/app/.output
      - /run/incomex:/run/incomex
    group_add:
      - "987"

4c. Recreate — CHỈ nuxt:

cd /opt/incomex/docker && docker compose up -d nuxt      # ★ BẮT BUỘC có chữ "nuxt"

🔴 TUYỆT ĐỐI KHÔNG docker compose up -d trần. Sẽ recreate agent-data mất 10 env (PG_*×5, DIRECTUS_DB_*×5) và hạ mem 2560→1536 MiB → hỏng KB/agent-data. (Đo: running=10 env, compose khai=0.)

4d. Verify wiring:

docker inspect incomex-nuxt --format '{{.State.Health.Status}}'      # healthy
docker exec incomex-nuxt ls -l /run/incomex/balo-reconcile.sock      # tồn tại
docker exec incomex-nuxt id                                          # groups phải chứa 987
docker inspect incomex-nuxt --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -c BALO   # 2
docker inspect incomex-agent-data --format '{{.HostConfig.Memory}}'  # 2684354560 — PHẢI không đổi

4e. Test công khai (gates):

curl -X POST …/api/balo/reconcile                              # 401 (no session)
curl -X POST -H 'Origin: https://evil.example' …               # 403
curl -X POST … (thiếu X-Incomex-Action)                        # 403
curl -X POST … -d 'x'                                          # 400
curl -X GET  …                                                 # 404/405

4f. Administrator clickALREADY_UP_TO_DATE. Singleton → 409. 4g. Fingerprint SAU == TRƯỚC2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1.

PHA 5 — COMMIT COMPOSE ⟨git-only⟩

git -C /opt/incomex add docker/docker-compose.yml
git -C /opt/incomex commit -m "compose(nuxt): mount balo bridge socket + group_add 987 + operator role ids"

Ma trận read-only / mutation

Pha Loại Rollback Bắt buộc tách?
0 READ-ONLY không
1 git-only (zero runtime) git revert không
2 MUTATION #1 (nginx + reload) file PRE + reload, ~5s không — tự chứng minh
3 git-only git revert không
4 MUTATION #2 (dir + compose + recreate) §12 không
5 git-only git revert không

Pha 2 phải trước pha 4 (route-first). Pha 1 phải trước pha 3 (không thì commit route gói cả drift).


11. MINIMAL PASS METRICS

Không có mục nào dựa vào trạng thái tạm.

# Gate PASS
1 Source/provenance 3 HEAD khớp; candidate tree == 2a2de2f1…; getent group incomex-balo-sync == 987
2 Nginx effective route nginx -t ok; nginx -T | grep -c 'location = /api/balo/reconcile' == 1; master PID == 1219625 (reload, không restart)
3 Public route provenance curl /api/balo/reconcileHTTP/1.1 404 **Page** Not Found + {"error":true,…}. CẤM {"detail":"Not Found"}. (Đo được ngay sau pha 2, chưa cần Nuxt)
4 Không hồi quy /api/reports/live == 400; / == 200; /api/ (agent-data) như cũ
5 Wiring container healthy; ls /run/incomex/balo-reconcile.sock ok; id chứa 987; đúng 2 biến *BALO*
6 Agent-data bất biến Memory == 2684354560; env PG_*/DIRECTUS_DB_* == 10
7 Auth/CSRF 401 / 403 / 403 / 400 / 404 đúng thứ tự gate
8 Administrator click ALREADY_UP_TO_DATE, HTTP 200
9 Singleton HTTP 409 (bridge ALREADY_RUNNING, rc=75)
10 Balo bất biến fp SAU == 2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1; --verifyPASS_RECONCILED|delta=0|write=0
11 Nút tự hiện Hiện trên 4/4 report balo_thuc_the; ẩn trên report non-balo
12 Rollback sẵn sàng nuxt-output.bak.pre-balo-$TS + compose.pre-balo-$TS.yml + default.conf.PRE tồn tại; image rollback có tag

★ = gate mới, chưa mission nào có.


12. ROLLBACK DESIGN

Nguyên tắc tối cao: rollback chỉ dựa vào FILE + IMAGE CÓ TAG. Không bao giờ dựa vào container đã stop (prune xoá trong ≤60′).

Hỏng ở Lệnh Thời gian Mất mát
Pha 2 (route) cp $B/default.conf.PRE → default.conf; nginx -t; nginx -s reload ~5s 0
Pha 4 (cutover) docker compose stop nuxt<br>rm -rf deploys/nuxt-output<br>mv deploys/nuxt-output.bak.pre-balo-$TS deploys/nuxt-output<br>cp backups/compose.pre-balo-$TS.yml docker/docker-compose.yml<br>docker compose up -d nuxt ~30s 0
Toàn bộ rollback pha 4 → rollback pha 2 → git reset --hard b5e0f96 ~1′ 0

Vì sao prune-proof:

  • nuxt-output.bak.pre-balo-$TS = thư mụcdocker system prune -f không đụng file.
  • Image rollback có tag (asia-southeast1-…/nuxt-ssr:latest, id 72715a92…) → prune -f chỉ xoá dangling; đo thật docker images -f dangling=truerỗng.
  • Compose backup = file.
  • Không mục nào cần container stopped sống sót.

Rollback dữ liệu: không cần. Đường ghi duy nhất là dot-pg-atomic-apply (SQL SHA-locked); PASS mong đợi là ALREADY_UP_TO_DATE (delta=0) → không có write nào xảy ra. Nếu fingerprint đổi ⇒ có gì đó rất sai ⇒ dừng, gọi Owner, không tự rollback DB.


13. REMAINING UNKNOWNS

# Unknown Confidence Có phải blocker?
1 2 deploy workflow còn disabled? (web-test 233946923, agent-data 244569412). gh trên VPS chưa auth → không kiểm được. Đã disable 07-15. KHÔNG cho deploy này (workflow ghi VPS, không ảnh hưởng pha 1–5). cho an toàn dài hạn: nếu bị bật lại, nó đắp nginx tháng 3 lên production (đúng cơ chế sự cố 11/07). Owner kiểm từ máy có auth.
2 Provenance artifact không có chứng thực mật mã — không nhúng git SHA; sourcemap sourcesContent:false. Tái dựng: cao. Mật mã: không có. KHÔNG (audit thiện chí). Đề xuất: stamp commit SHA vào nitro.json cho build sau.
3 NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS override tới client payload — Nuxt 3 serialize runtimeConfig.public vào SSR payload lúc request; INFERENCE (cao), chưa chạy thật. Tiền lệ: NUXT_PUBLIC_DIRECTUS_URL đang hoạt động y hệt. cao KHÔNG — gate #11 bắt được ngay tại chỗ. Nếu FAIL: cần rebuild với env set (mất §5) — Owner sẽ được báo.
4 Disk 91%, prune mỗi giờ, thu hồi ~0 (91%→91%) FACT KHÔNG cho deploy (rollback §12 prune-proof). cho vận hành: mọi canary/blue-green tương lai sẽ bị xoá. Đề xuất Owner mở phiên riêng. Ngoài phạm vi read-only.
5 GID 987 do OS cấp, ghim cứng trong compose FACT KHÔNG — gate #1 (getent) bắt. Nhưng nếu group bị tạo lại với GID khác → container EACCES âm thầm.
6 BrokenPipeError chưa bắt tại dot-balo-reconcile-bridge:167 (sendall) — client ngắt trước khi đọc reply. Thread là daemon, service sống. FACT KHÔNG — mỹ phẩm. Backlog.
7 Bridge .service không có Restart= FACT KHÔNG — socket-activated. Backlog.

Ai xoá 2 container? — ĐÃ GIẢI, không còn UNKNOWN. docker system prune -f từ cron disk-monitor.sh, 02:00:01 CEST 17/07. ID khớp chính xác evidence 3D.2. Không phải người, không phải deploy script — script chỉ stop, prune xoá 2′26″ và 4′44″ sau. Và: việc không biết ai xoá chưa bao giờ là blocker thật — provenance nằm ở commit + tree hash, cả hai còn nguyên.


14. FACTS CODEX MAY RELY ON

Mọi dòng dưới đây đã đo thật trong báo cáo này (không phải trích lại báo cáo cũ).

git HEAD dot        = de4730013ca3cee5f55cb54c00fe51f1c2f1068b
git HEAD nuxt-repo  = 2ea41583422427b6b5e6d886fa7a754198e121b7
git HEAD /opt/incomex = b5e0f962600c4e42969df1e940c9c981254f0175  (branch feat/s177-sprint1-round-a)
/opt/incomex        : KHÔNG REMOTE, không script nào auto-commit/push nó

nginx SSOT          = /opt/incomex/docker/nginx/conf.d/default.conf
nginx live sha256   = 3b947f4d0b61ccdf7dd194534fc39ba2721ef8a76d134d7d3785f9969cad1cc6
   (host == container == effective; mount RO; nginx -t ok; master PID 1219625, RestartCount 0)
nginx pre-patch bak = /opt/incomex/backups/nginx-ssot/20260715T094658Z/default.conf.LIVE-SNAPSHOT (9404dce8…)
diff live vs HEAD   = 9+/26−; strip comment ⇒ CHỈ 1 hunk (/.well-known); CẢ HAI đều 404
route balo          = KHÔNG TỒN TẠI (nginx -T grep = 0)
điểm chèn           = sau dòng 242 (đóng /api/reports/), trước dòng 244 (/api/registry/)
tiền lệ exact       = location = /api/openapi.json ; tiền lệ prefix→nuxt = b5e0f96 (/api/reports/)

/api/balo/reconcile HIỆN TẠI = Agent Data:  HTTP/1.1 404 Not Found       {"detail":"Not Found"}
Nuxt 404 (đối chứng)         =              HTTP/1.1 404 Page Not Found  {"error":true,…,"statusMessage":"Page Not Found"}

prod nuxt: id 890fee21392c  Created 2026-07-11T02:50:31Z  StartedAt 2026-07-16T23:57:35Z
           RestartCount=0  ExitCode=0  healthy  mem 512m (== compose)
           mount DUY NHẤT: /opt/incomex/deploys/nuxt-output -> /app/.output (rw)
           GroupAdd=[]   user nuxtjs uid=1001 gid=65533(nogroup)
           env: KHÔNG có biến nào chứa "balo"
           /run/incomex: KHÔNG TỒN TẠI trong container
compose nuxt ↔ runtime: KHỚP (mọi env key có; mem 512m==512m) ⇒ up -d nuxt an toàn cho nuxt

socket  /run/incomex/balo-reconcile.sock  660 root:incomex-balo-sync
group   incomex-balo-sync = GID 987  (0 member — không sao, group_add tiêm thẳng GID)
dir     /run/incomex = 0750 root:incomex-balo-sync  (cần 987 để traverse)
unit    SocketGroup=incomex-balo-sync (tên, KHÔNG phải số) — enabled+active
        host nogroup=65534 ; container nogroup=65533  ⇒ 65533 KHÔNG tồn tại trên host (gốc lỗi 3D)

dot-balo-reconcile --dry-run = DRY_RUN|PASS|delta=0|write=0        exit=0
dot-balo-reconcile --verify  = VERIFY|PASS_RECONCILED|delta=0|write=0  exit=0
singleton = flock -n /run/lock/incomex-balo-reconcile-sync.lock ⇒ ALREADY_RUNNING rc=75
nơi write duy nhất = dot-pg-atomic-apply (SQL SHA-locked, manifest c018f12c…)

balo_thuc_the count = 2148   (BEGIN TRANSACTION READ ONLY)
fingerprint runner  = 2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1
role Administrator  = a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac  (LIVE, 1 active user)  ← ĐÃ XÁC MINH

candidate = /opt/incomex/deploys/nuxt-output-balo-20260716T234206Z
            1713 files, 43M, content-fp 2a2de2f160e27062ca1e0ee09da67cbc1ea83d1971bac8db44a191efe4373d1f
            == web/.output trong repo (diff -rq rỗng); build 23:48:03Z; không đổi sau đó
            layout: nitro.json/public/server ở TOP LEVEL (KHÔNG có wrapper .output/)
env var cần: NUXT_BALO_OPERATOR_ROLE_IDS (endpoint) + NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS (nút hiện)
socket path : mặc định /run/incomex/balo-reconcile.sock — KHÔNG cần set

container bị xoá bởi: cron 0 * * * * disk-monitor.sh → docker system prune -f  (02:00:01 CEST 17/07)
                      ID b4fcfe38…(candidate) + e512f557…(canary) — khớp evidence 3D.2
disk = 91% (87G/96G, 8.9G trống) > THRESHOLD 85 ⇒ prune MỖI GIỜ, VĨNH VIỄN, thu hồi ~0
prune -f KHÔNG xoá image có tag (chỉ dangling; đo: dangling = rỗng) ⇒ image rollback AN TOÀN
agent-data: running mem 2684354560 (2560MiB) vs compose 1536m; running env PG_*/DIRECTUS_DB_* = 10 vs compose 0

15. ASSUMPTIONS CODEX MUST NOT MAKE

❌ "Chỉ cần thêm Nginx route + cutover là xong."
   → SAI. Còn 3 blocker trong docker-compose.yml: socket mount, group_add 987,
     và 2 biến env. Thiếu env ⇒ nút VÔ HÌNH dù mọi thứ khác PASS.

❌ "Set NUXT_BALO_OPERATOR_ROLE_IDS là đủ."
   → SAI. Endpoint đọc private key; NÚT đọc config.public.baloOperatorRoleIds
     (BaloSyncButton.vue:19, v-if="canSync" @:69). Runtime cần CẢ HAI biến.
     Chỉ set biến private ⇒ endpoint chạy nhưng nút không hiện.

❌ "Chạy `docker compose up -d` cho tiện."
   → CỰC KỲ NGUY HIỂM. Sẽ recreate agent-data MẤT 10 env (PG_*×5, DIRECTUS_DB_*×5)
     và hạ mem 2560→1536 MiB ⇒ hỏng KB/agent-data. BẮT BUỘC `docker compose up -d nuxt`.

❌ "Container canary/failed phải tồn tại thì mới đủ bằng chứng."
   → Gate BẤT KHẢ THI. prune chạy mỗi giờ, disk 91% > 85% vĩnh viễn ⇒ container
     stopped chết trong ≤60′. Provenance = commit + tree hash, KHÔNG phải container.

❌ "Rollback = start lại container cũ."
   → SAI. Container stopped sẽ bị prune xoá. Rollback CHỈ dựa vào file + image có tag.

❌ "Nginx dirty nghĩa là có thay đổi lạ, không rõ nguồn."
   → SAI. 25/35 dòng là comment; 1 block chức năng, cả hai phía đều 404; truy được
     100% về deploy 11/07 + patch OAuth 07-15 (sha 9404dce8→3b947f4d).

❌ "Commit baseline có rủi ro production."
   → SAI. File trên đĩa không đổi 1 byte, không reload, repo không remote.
     Không có mặt runtime nào.

❌ "Commit baseline sẽ đẩy divergence lên GitHub (vi phạm B5)."
   → SAI. /opt/incomex KHÔNG có remote; script push chỉ đụng nuxt-repo + agent-data-repo.
     Lệnh cấm B5 nhắm hướng GitHub → VPS, không phải commit local.

❌ "nuxt-repo/infra/nginx là SSOT của Nginx."
   → SAI VÀ NGUY HIỂM. Nó là fork CHẾT từ 2026-03-30 (d753fa0c) và ĐƯỢC AUTO-PUSH
     lên GitHub 2 lần/ngày. Đồng bộ nó → VPS = đắp nginx tháng 3 lên production
     = tái diễn đúng sự cố 11/07. SSOT DUY NHẤT = docker/nginx/conf.d/default.conf.

❌ "Thứ tự location quyết định /api/balo thắng /api/ catch-all."
   → SAI về kỹ thuật. Prefix location = LONGEST MATCH, không theo thứ tự. Chỉ regex
     mới theo thứ tự. `=` exact thắng tuyệt đối và short-circuit luôn regex.

❌ "Phải thêm rewrite như block /api/ catch-all."
   → SAI. Catch-all có `rewrite ^/api/(.*) /$1 break` cho Agent Data. Nuxt cần
     NGUYÊN path /api/balo/reconcile. KHÔNG rewrite. (Mirror block /api/reports/.)

❌ "Rebuild Nuxt cho chắc."
   → PHẢN TÁC DỤNG. buildId = UUID ngẫu nhiên + timestamp ⇒ KHÔNG BAO GIỜ diff
     byte-equal ⇒ mất neo kiểm chứng. Và `npm ci` sẽ dùng package-lock.json (stale)
     pin nuxt 3.20.1 ≠ pnpm-lock 3.20.2 của artifact.

❌ "Candidate có wrapper .output/."
   → SAI. nitro.json/public/server nằm TOP LEVEL. Mount trỏ thẳng vào thư mục đó.

❌ "` M web-rp-current` là drift cần xử lý."
   → KHÔNG. Đó là linked git worktree (không .gitmodules), sibling của web/, KHÔNG
     phải build input. Untracked files bên trong. Vô hại.

❌ "systemd không nhận numeric GID (bài học 3D)."
   → SAI về nguyên nhân. systemd fail vì GID 65533 KHÔNG TỒN TẠI trên host
     (host nogroup=65534). Bài học đúng: SocketGroup cần identity host-resolvable,
     và 65533/nogroup còn SAI VỀ BẢO MẬT (catch-all).

❌ "systemd-analyze verify PASS nghĩa là unit chạy được."
   → SAI. Nó là kiểm TĨNH; lỗi resolve group xảy ra lúc ACTIVATION. Chính false-green
     này đã tốn trọn mission 3D.

❌ "Đo route bằng status code là đủ."
   → SAI. Cả hai đều 404. Phải so BODY + REASON PHRASE:
     Nuxt = "404 Page Not Found" + {"error":true,…}
     Agent Data = "404 Not Found" + {"detail":"Not Found"}

❌ "GID 987 là hằng số."
   → SAI. Do OS cấp. Ghim trong compose ⇒ nếu group bị tạo lại với GID khác,
     container EACCES ÂM THẦM. Preflight `getent group incomex-balo-sync` == 987 BẮT BUỘC.

❌ "Cần canary để chứng minh route."
   → KHÔNG. Route tự chứng minh với Nuxt production hiện tại (§8.1). Canary tốn kém,
     bị prune xoá, và 3D.2 đã làm production down 2′16″ vì nó.

16. EVIDENCE PATHS

Runtime (đo trực tiếp phiên này, 2026-07-17 02:44Z–03:10Z):

git -C {/opt/incomex, /opt/incomex/dot, /opt/incomex/docker/nuxt-repo} rev-parse HEAD / status / log / diff / show b5e0f96
sha256sum /opt/incomex/docker/nginx/conf.d/default.conf
docker inspect {incomex-nuxt, incomex-nginx, incomex-agent-data}
docker exec incomex-nginx nginx -T / nginx -t
docker exec postgres psql -U directus -d directus  (BEGIN TRANSACTION READ ONLY)
curl -D- https://vps.incomexsaigoncorp.vn/api/balo/reconcile
curl -D- https://vps.incomexsaigoncorp.vn/api/reports/definitely-not-a-route
/opt/incomex/dot/bin/dot-balo-reconcile --dry-run | --verify
getent group incomex-balo-sync ; systemctl cat incomex-balo-reconcile.socket
crontab -l ; df -h /

Filesystem:

/opt/incomex/docker/nginx/conf.d/default.conf                      3b947f4d…  ★ SSOT
/opt/incomex/docker/docker-compose.yml                             (dirty, human-authored)
/opt/incomex/backups/nginx-ssot/20260715T094658Z/                  default.conf.LIVE-SNAPSHOT 9404dce8…
/opt/incomex/deploys/nuxt-output                                   production (nitro 2026-07-11T02:49:53Z)
/opt/incomex/deploys/nuxt-output-balo-20260716T234206Z             candidate 2a2de2f1…
/opt/incomex/deploys/nuxt-output-balo-current-path                 (con trỏ, 55B — không report nào giải thích)
/opt/incomex/deploys/infra-prev/nginx/conf.d/default.conf          9404dce8… (bản deploy 11/07)
/opt/incomex/docker/nuxt-repo/infra/nginx/conf.d/default.conf      d753fa0c… ⚠️ FORK CHẾT 2026-03-30
/opt/incomex/logs/disk-monitor.log                                 ★ lời thú tội của prune
/opt/incomex/scripts/disk-monitor.sh                               THRESHOLD=85, prune -f
/opt/incomex/scripts/git-push-gh-daily-v2.sh                       chỉ nuxt-repo + agent-data-repo
/opt/incomex/evidence/lenh3d2-balo-button-production-20260716T234206Z/
    canary-create-id.txt = e512f557…      production-new-create-id.txt = b4fcfe38…
    production-gates-preclick.txt (toàn 404)   rollback-old-start.txt
/opt/incomex/docker/nuxt-repo/web/server/api/balo/reconcile.post.ts        3423f16c…
/opt/incomex/docker/nuxt-repo/web/server/utils/baloReconcileContract.mjs   (gate logic)
/opt/incomex/docker/nuxt-repo/web/components/BaloSyncButton.vue            (:19 public, :69 v-if)
/opt/incomex/docker/nuxt-repo/web/components/shared/DirectusTable.vue      (v-if resolvedCollection)
/opt/incomex/docker/nuxt-repo/web/nuxt.config.ts                           (:4, :83-88, :109)

KB (7 báo cáo tiền nhiệm — CHỈ tồn tại trong KB, KHÔNG có trên filesystem):

codex-lenh-3a-nuxt-source-production-superset-2026-07-16.md      READY_FOR_REPORT_RECENT_IMPLEMENTATION
codex-lenh-3b-balo-sync-button-2026-07-16.md                     HOLD (bridge/auth/singleton)
codex-lenh-3c-balo-sync-candidate-2026-07-16.md                  READY_FOR_..._DEPLOY_GATE  ⚠️ false green SocketGroup
codex-lenh-3d-balo-sync-production-2026-07-17.md                 HOLD — BRIDGE_INSTALL_FAILED
codex-lenh-3d1-balo-socket-group-bridge-production-2026-07-17.md READY (PASS duy nhất của series)
codex-lenh-3d2-balo-button-production-2026-07-17.md              ROLLED_BACK — NGINX_ROUTE_REQUIRED
codex-lenh-3d3-exact-nginx-balo-route-2026-07-17.md              HOLD — PRE_ROUTE_DRIFT

⚠️ path trong các báo cáo là KB path, không phải filesystem pathfind / -name 'codex-lenh-3*'0 hit.

Memory liên quan (đã đối chiếu khớp): project_kb_hang_phase2a_oauth_patch_APPLIED_2026_07_15.md — sha 9404dce8…3b947f4d… khớp chính xác live.


17. CHỨNG MINH PHẠM VI CẤM

NGINX_CHANGE=NO          NGINX_RELOAD=NO         NUXT_BUILD=NO
CONTAINER_MUTATION=NO    SYSTEMD_CHANGE=NO       BALO_WRITE=NO
DB_WRITE=NO              GIT_COMMIT=NO           GITHUB_PUSH=NO

Chứng cứ:

  • nginx sha trước == sau == 3b947f4d…; master PID 1219625 không đổi; RestartCount=0.
  • Chỉ chạy dot-balo-reconcile --dry-run / --verify (được phép) → write=0, FINGERPRINT|unchanged=PASS.
  • Mọi truy vấn PG trong BEGIN TRANSACTION READ ONLY.
  • docker ps -a không đổi; không tạo/xoá/start/stop container nào.
  • git status không đổi; 0 commit; /opt/incomex không có remote → push bất khả thi.
  • Không gửi BALO_SYNC_V1 tới socket.
  • Không cài package. Không sửa systemd/compose/source.

DỪNG SAU BÁO CÁO. Không soạn, không thực thi prompt Codex.

Back to Knowledge Hub knowledge/current-state/reports/claude-balo-button-deployment-forensic-and-recommendation-2026-07-17.md