KB-988F

Claude — Nút Đồng bộ Balo: triển khai production PASS (2026-07-17)

34 min read Revision 1
balonuxtnginxdeploymentproductionone-clickbridgecurrent-statePASS

NÚT ĐỒNG BỘ BALO — TRIỂN KHAI PRODUCTION MỘT LẦN CÓ KIỂM SOÁT

  • Ngày: 2026-07-17 (03:37Z–04:05Z)
  • Thực hiện: Claude Opus 4.8 (1M)
  • Nguồn điều hành: knowledge/current-state/reports/claude-balo-button-deployment-forensic-and-recommendation-2026-07-17.md (verdict READY_FOR_SINGLE_CONTROLLED_IMPLEMENTATION)
  • Mission TS: 20260717T033701Z

1. VERDICT

PASS_BALO_ONE_CLICK_SYNC_PRODUCTION

Đủ 12/12 phép đo §19. Không HOLD. Không rollback. Chuỗi HOLD 3D → 3D.1 → 3D.2 → 3D.3 đã chấm dứt.

Kết quả cho Owner: Administrator vào bất kỳ report Balo nào, thấy nút "Đồng bộ Balo từ PG", bấm một lần → Balo đã mới nhất, bảng tự refresh tại chỗ, dữ liệu không đổi (delta=0, write=0). Nút tự áp cho 4 report Balo hiện tạimọi report Balo tương lai dùng collection='balo_thuc_the' — chỉ cần một dòng registry, không sửa code, không thêm URL.

Ba điều dự đoán trong forensic đã được production xác nhận:

  1. Route tự chứng minh được trước khi đụng Nuxt — không cần canary. Đúng.
  2. Commit baseline Nginx là git thuần, không đổi 1 byte file live, không reload. Đúng.
  3. Ba blocker thật nằm ở compose, không phải Nginx. Đúng — và điều kiện #3 (NUXT_PUBLIC_*) đúng là thứ sẽ gây HOLD thứ tư nếu không phát hiện trước.

Bốn việc phải nói thẳng (chi tiết §18):

  • Một lỗi của tôi: rò rỉ secret ra file evidence khi so sánh compose sai cách — đã phát hiện, shred, và làm lại bằng --no-interpolate.
  • Một giả định sai của tôi đã bị đo thật bác bỏ (cookie host-only → tưởng sẽ 401).
  • Canonical v1.9 §15 vốn CẤM "nút UI đồng bộ Balo" — đã sửa tường minh, không lặng lẽ bỏ qua.
  • Hai UNKNOWN còn nguyên (workflow deploy cũ; disk 91%).

2. CURRENT-STATE / PREFLIGHT

Toàn bộ invariant bền vững khớp trước mọi mutation:

# Hạng mục Đo Khớp
1 DOT HEAD de4730013ca3cee5f55cb54c00fe51f1c2f1068b
1 Nuxt HEAD 2ea41583422427b6b5e6d886fa7a754198e121b7
1 Root repo HEAD b5e0f96… branch feat/s177-sprint1-round-a
2 Bridge socket active; mode=660 root:incomex-balo-sync gid=987
3 Host group incomex-balo-sync:x:987:
4 Nuxt prod inspect GroupAdd=[], mount chỉ nuxt-output, 0 env balo ✅ (đúng 3 blocker)
5 Compose service nuxt (từ docker compose config --services)
6 Nginx SSOT chain host → bind RO → container → nginx -T
7 Nginx live SHA 3b947f4d0b61ccdf7dd194534fc39ba2721ef8a76d134d7d3785f9969cad1cc6
8 Prod output 1708 files, fp 679ea3de…, nitro 2026-07-11T02:49:53Z, 0 balo route
9 Balo baseline 2148 = table 385 + view 699 + function 654 + trigger 410 ✅ khớp dự kiến
9 Fingerprint 2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1
9 date_updated hash ae41989817f47ca97cf4a07b3cb89c5d pinned
9 rows md5 c2cce083b9c5535d48431c4578fa0e2a pinned
10 dry-run / verify DRY_RUN|PASS|delta=0|write=0 · VERIFY|PASS_RECONCILED|delta=0|write=0
11 disk-monitor prune 0 * * * *, THRESHOLD=85, disk 91% → prune mỗi giờ ✅ (đã tính vào rollback)

Live count không đổi so với dự kiến ⇒ giữ 2148 làm baseline đo được (không hardcode).


3. NGINX: BASELINE + EXACT ROUTE

3.1 Baseline — commit 4ecb614249d913d42fc5bfed5de5e479af7b9e67

Theo đúng phương án A mà forensic khuyến nghị (nhận toàn bộ live làm baseline, commit riêng, trước route).

Chứng minh zero-runtime — đo trước/sau chính commit đó:

SHA trước commit : 3b947f4d…  (default.conf)   7f243336…  (docker-compose.yml)
SHA sau  commit  : 3b947f4d…  (default.conf)   7f243336…  (docker-compose.yml)   ← Y HỆT
nginx PID trước  : 1219625
nginx PID sau    : 1219625                                                        ← Y HỆT

2 files changed, 89 insertions(+), 87 deletions(-) — thuần Git. File live không đổi 1 byte, không reload, repo local-only (không remote, không script nào auto-push root repo).

3.2 Exact route — commit 78b523c3797beb48fc36230702b0bdceb4ced9d5

Chèn sau dòng 242 (đóng /api/reports/), trước /api/registry/, trong server vps.incomexsaigoncorp.vn:443. Mirror đúng proxy-header pattern của các Nuxt API route hiện hành. 12 insertions(+).

    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 mở prefix rộng. Không sửa location /api/. Không rewrite (catch-all có rewrite ^/api/(.*) /$1 cho Agent Data; Nuxt cần nguyên path). Không CORS wildcard, không auth bypass, không proxy thẳng tới Unix socket.

nginx -t PASS → reload → master PID 1219625 không đổi, RestartCount=0 (reload chứ không restart).

3.3 ★ Route tự chứng minh TRƯỚC khi deploy Nuxt

Chạy khi production vẫn là Nuxt (chưa có handler balo):

TRƯỚC reload:  HTTP/1.1 404 Not Found        {"detail":"Not Found"}                    ← Agent Data
SAU   reload:  HTTP/1.1 404 Page Not Found   {"error":true,...,"statusMessage":"Page Not Found"}   ← NUXT

GET và POST đều cho cùng chữ ký Nuxt. Đây là bằng chứng route hoạt động mà không cần build, không cần canary, không cần cutover — chính là thứ 3D.2 đã phải trả giá bằng 2′16″ production downtime để biết.

Không hồi quy (đo cùng lúc):

/api/reports/live               400   (Nuxt, như trước)
/api/registries-pivot/summary   200   (Nuxt)
/                               200   (Nuxt)
/api/health                     200   (Agent Data)
/api/nonexistent                {"detail":"Not Found"}   (Agent Data NGUYÊN VẸN)
nginx -T | grep -c "location = /api/balo/reconcile"  =  1

4. COMPOSE NUXT CONTRACT — TRƯỚC / SAU

Commit 7a9843d · 16 insertions(+) · chỉ service nuxt.

TRƯỚC SAU
volumes nuxt-output:/app/.output + /run/incomex:/run/incomex:**ro**
group_add (không có) ["987"]
env balo (không có) NUXT_BALO_OPERATOR_ROLE_IDS, NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS, NUXT_BALO_RECONCILE_SOCKET_PATH
image / mem / healthcheck / network (không đổi) (không đổi)

★ Chứng minh chỉ nuxt đổi contract

docker compose config --no-interpolate (không render secret), so hash từng service:

postgres    UNCHANGED  8857d895a41a8fe4
qdrant      UNCHANGED  e57f4df03deb9327
directus    UNCHANGED  ede7d7d0e215d30f
agent-data  UNCHANGED  51027778b3eef9b5     ← quan trọng nhất
nginx       UNCHANGED  9f1473a86ab0bd37
nuxt        CHANGED    f3fc0d97a9993421 -> 7252cbcad903ef91   (đúng kỳ vọng)

docker compose config exit=0, 0 cảnh báo thiếu biến. NUXT_PUBLIC_SITE_URL render đúng https://vps.incomexsaigoncorp.vn.

SSOT env

/opt/incomex/docker/.env (mode 0600 root) — compose đọc mặc định vì nằm cạnh compose file. WEB_URL=https://vps.incomexsaigoncorp.vn nằm ở đây (không/opt/incomex/.env).

Không đưa secret xuống client. Role ID không phải secret → để literal trong compose (nằm trong git ⇒ provenance rõ) thay vì .env; điều này cũng tránh đụng .env vốn có cron env-permissions-guard.sh chạy mỗi 5 phút. Public/private contract vẫn giữ đúng: private cho endpoint, public chỉ để hiện nút.


5. HAI ROLE CONFIG — PRIVATE VÀ PUBLIC

Tên biến đọc từ source, không đoán (nuxt.config.ts:4, 83-88, 109):

const baloOperatorRoleIds = process.env.NUXT_BALO_OPERATOR_ROLE_IDS || '';
runtimeConfig: {
  baloOperatorRoleIds,                                   // PRIVATE → endpoint
  baloReconcileSocketPath: process.env.NUXT_BALO_RECONCILE_SOCKET_PATH || '/run/incomex/balo-reconcile.sock',
  public: { baloOperatorRoleIds }                        // PUBLIC → hiện nút
}

Source chỉ có MỘT biến build-time nuôi CẢ HAI key ⇒ lúc chạy phải override bằng HAI biến (Nuxt: private ← NUXT_*, public ← NUXT_PUBLIC_*):

Key Biến runtime Ai đọc Thiếu thì
baloOperatorRoleIds NUXT_BALO_OPERATOR_ROLE_IDS authorizationGate (endpoint) 503 HOLD_AUTH_NOT_CONFIGURED
public.baloOperatorRoleIds NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS BaloSyncButton.vue:19v-if="canSync" (:69) NÚT VÔ HÌNH

Không cần sửa source — mapping đã đóng kín qua cơ chế override chuẩn của Nuxt; tiền lệ NUXT_PUBLIC_DIRECTUS_URL/NUXT_PUBLIC_SITE_URL đang chạy y hệt. Đã kiểm cả server lẫn client: server → 401/403 đúng gate; client → nút hiện (§8). Artifact giữ env-neutral: build không set biến balo, admin UUID không bị bake (grep = 0).

Role: a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac = Administrator, xác minh live (directus_roles), 1 active user. Allowlist rỗng ⇒ fail-closed.


6. SOCKET MOUNT + SUPPLEMENTARY GROUP

Chọn :ro — đo thật, không suy luận. Container disposable, không gửi BALO_SYNC_V1 (chỉ connect rồi đóng):

:ro + --group-add 987   →  CONNECT_OK      (kernel không áp EROFS lên socket inode)
:ro, KHÔNG group-add    →  CONNECT_FAIL: EACCES   (đối chứng: group thật sự load-bearing)
bridge sau test         →  active active

Kết quả trên production sau deploy:

Mounts:  /opt/incomex/deploys/nuxt-output -> /app/.output  rw=true
         /run/incomex                     -> /run/incomex  rw=false      ← :ro
id:      uid=1001(nuxtjs) gid=65533(nogroup) groups=987,65533(nogroup)   ← 987 CÓ
socket:  srw-rw----  1 root  987  /run/incomex/balo-reconcile.sock       ← container NHÌN THẤY

⚠️ GID 987 do OS cấp, ghim cứng trong compose ⇒ preflight getent group incomex-balo-sync == 987bắt buộc; nếu group bị tạo lại với GID khác, container sẽ EACCES âm thầm. Đã ghi cảnh báo này ngay trong comment compose và canonical §16.4.


7. BUILD ARTIFACT

Build mới từ source commit đã khoá (theo §9 lệnh), bằng pnpm để tránh bẫy đã cảnh báo trong forensic:

source HEAD    = 2ea4158…   git status -- web/ : SẠCH
4 file tính năng balo: PRESENT tại HEAD
typecheck      = PASS (exit 0)
build          = PASS (exit 0), 3m00s, Σ 35.5 MB
nitro date     = 2026-07-17T03:44:50.661Z
staged         = /opt/incomex/deploys/nuxt-output-balo-fresh-20260717T033701Z
files=1713  du=43M  content-fp = 0454f85d6496fc912c93a4af94c57c4409b3f3487394c585585538eff25e0dc0

Bẫy đã né: web/package-lock.json (tracked, stale) pin nuxt 3.20.1; pnpm-lock.yaml (governing, packageManager: pnpm@9.6.0) pin 3.20.2 = đúng version trong node_modules. Dùng corepack pnpm run build. Nếu chạy npm ci sẽ âm thầm build sai version.

Kiểm nội dung

balo route .mjs   : server/chunks/routes/api/balo/reconcile.post.mjs      ← CÓ
Matrix            : server/chunks/routes/api/registry/matrix.get.mjs      ← CÒN
reports/live      : server/chunks/routes/api/reports/live.get.mjs         ← CÒN
API handler count : 40 → 41    (+1 = đúng endpoint Balo)
route .mjs count  : 44 → 45    (+1)
endpoint chỉ POST : .post.mjs = 1 ; .get/.put/.delete balo = 0
admin UUID baked  : 0          ← env-neutral, ĐÚNG
hardcode report-balo-* trong balo chunk : 0
BaloSyncButton    : server/chunks/build/DirectusTable-Ddo3jnxr.mjs        ← CÓ

★ Rebuild xác nhận ngược lại candidate cũ không bị tamper

Fresh build vs candidate nuxt-output-balo-20260716T234206Z khác đúng 7 file, tất cả do buildId ngẫu nhiên:

nitro.json · public/_nuxt/builds/latest.json · builds/meta/<uuid>.json (mỗi bên 1)
server/chunks/build/styles.mjs (+.map) · server/chunks/nitro/nitro.mjs
buildId candidate = ff2db2ab-0ad9-43b6-87b1-7b4634661c27
buildId fresh     = f4591241-9370-4419-82aa-e8adff03343d

server/chunks/routes/api/balo/reconcile.post.mjs : IDENTICAL
server/chunks/build/DirectusTable-Ddo3jnxr.mjs   : IDENTICAL

⇒ Logic nghiệp vụ byte-identical. Một build độc lập từ commit đã khoá đã chứng minh ngược lại rằng candidate 3C/3D.2 là thật — đúng như verdict A của forensic, dù mission này chọn build mới. Candidate cũ được giữ nguyên làm provenance.


8. DEPLOY CHỈ NUXT — VÀ CHỨNG MINH AGENT DATA ZERO-TOUCH

docker compose config --services  →  qdrant agent-data postgres directus nuxt nginx
cd /opt/incomex/docker && docker compose up -d nuxt          ← CÓ chữ "nuxt"

Output: Container incomex-nuxt Recreate → Recreated → Starting → Started. postgres/incomex-directus chỉ Running/Waiting/Healthy (kiểm depends_on) — không recreate.

★ Agent Data — bốn chiều, không chiều nào đổi

ID        4220cc88116e00684fb73aab2be277649359a742ea092c76998f0195f2db60e5   == PRE  ✅ không recreate
StartedAt 2026-05-19T08:26:16.229327572Z                                     == PRE  ✅ không restart
Mem       2684354560 (2560 MiB)                                              == PRE  ✅ không hạ 1536
ENV sha   b85fe5867d110192468da5fda90784be33a0740beec11eea5a16d11a8e5073e9   == PRE  ✅ 10 env nguyên
PG_/DIRECTUS_DB_ count = 10                                                  == PRE  ✅

Các service khác — uptime không reset

incomex-agent-api-executor  Up 6 weeks     incomex-agent-data  Up 8 weeks
incomex-claude-kb  Up 25 hours             incomex-claude-mcp  Up 5 days
incomex-directus   Up 2 months            incomex-qdrant      Up 4 months
postgres  Up 3 months                     uptime-kuma  Up 3 months
incomex-nginx  Up 2 weeks   (Pid=1219625 == PRE, RestartCount=0)   ← không restart
incomex-nuxt   Up 19 minutes (healthy)     RestartCount=0          ← chỉ container này

Bridge không restart ngoài socket-activation bình thường.

Nuxt sau deploy

ID=42079c55bf9ccd6fd2cb33562c4bea07c81e15cf972e6c0ef24a9b3372175b8f
Health=healthy  RestartCount=0  Image=sha256:72715a92…  (image KHÔNG đổi)
GroupAdd=[987]  Mem=536870912
env: NUXT_BALO_OPERATOR_ROLE_IDS / NUXT_PUBLIC_BALO_OPERATOR_ROLE_IDS / NUXT_BALO_RECONCILE_SOCKET_PATH

9. PUBLIC ENDPOINT PROVENANCE

/                                200      /reports/report-balo-table      200
/knowledge/pivot                 200      /reports/report-balo-view       200
/reports                         200      /reports/report-balo-function   200
/reports/report-pg               200      /reports/report-balo-trigger    200
/api/registries-pivot/summary    200      /api/reports/live               400 (như trước)
/api/health (Agent Data)         200      /api/nonexistent  {"detail":"Not Found"}  (Agent Data nguyên)

Matrix hoạt động, Reports refresh hoạt động, PG Census "Làm mới" hoạt động, 4 report Balo đọc đúng dữ liệu, generic Agent Data API không bị ảnh hưởng.


10. ADMINISTRATOR UI / BUTTON VERIFICATION

Phiên Administrator thật — đăng nhập qua UI app (/auth/signin, form input[name=email]/[name=password]), không dùng service token giả user. Chromium headless (playwright 1.57 + chromium-1223 đã có sẵn, dùng executablePath; không cài package).

Một giả định của tôi đã bị đo thật bác bỏ

Tôi thấy Set-Cookie: directus_session_token=…; HttpOnly; SameSite=Lax không có Domain= ⇒ host-only cho directus.incomexsaigoncorp.vn ⇒ tôi lo endpoint ở vps.* sẽ luôn 401. Đo bằng browser thật:

url sau login = https://vps.incomexsaigoncorp.vn/portal
cookie:  session                domain=vps.incomexsaigoncorp.vn
         directus_session_token domain=vps.incomexsaigoncorp.vn  httpOnly=true sameSite=Lax   ← CÓ
         __session              domain=vps.incomexsaigoncorp.vn
★ directus_session_token có trên vps.* ? CÓ (endpoint nhận được)

Vì app login qua proxy same-origin server/api/directus/[...path].ts — proxy này xoá Domain attribute ("so browser uses current domain"). Giả định sai, đã bị bằng chứng bác bỏ trước khi thành kết luận.

Nút hiện đúng chỗ

/reports/report-balo-table      nút=1  ✅ HIỆN
/reports/report-balo-view       nút=1  ✅ HIỆN
/reports/report-balo-function   nút=1  ✅ HIỆN
/reports/report-balo-trigger    nút=1  ✅ HIỆN
/reports/report-pg              nút=0  ✅ ẨN ĐÚNG (PG Census)
/reports                        nút=0  ✅ ẨN ĐÚNG
không đăng nhập                 nút=0  ✅ ẨN ĐÚNG

Ghi chú phương pháp: SSR HTML không chứa nút — đúng thiết kế, vì modules/directus/runtime/plugins/auth.tsif (import.meta.server) return; nên user chỉ fetch phía client. curl về nguyên tắc không thể kiểm nút này; phải browser thật. Nếu chỉ dùng curl sẽ kết luận sai là FAIL.

Server xác nhận đúng role

/users/me với phiên đó: status=active, role = Administrator / a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac ⇒ khớp allowlist. Endpoint tự xác thực lại phía server (không tin client).

Auth / CSRF gates (đo qua public endpoint)

POST không session (đủ header)   → 401 UNAUTHENTICATED
POST sai Origin                  → 403 ORIGIN_REJECTED
POST thiếu Origin                → 403
POST thiếu X-Incomex-Action      → 403 ACTION_HEADER_REJECTED
POST sai Sec-Fetch-Site          → 403
POST body không rỗng             → 400 BODY_MUST_BE_EMPTY
GET / PUT                        → 404   (§12 cho phép 404/405; Nuxt chỉ đăng ký .post)

Kế thừa cho Balo tương lai (chứng minh từ logic + runtime registry, không tạo report thử)

pages/reports/  →  index.vue  +  [report]/index.vue        ← route ĐỘNG, không có file/URL per-report
cổng duy nhất  →  DirectusTable.vue:737-739
                  <BaloSyncButton v-if="resolvedCollection === 'balo_thuc_the'" @synced="handleRefresh" />
resolvedCollection = registryConfig.collection  ← đọc table_registry lúc chạy (readItems('table_registry'))

table_registry (runtime):
  tbl_balo_function | balo_thuc_the | active | view_sql=(null)
  tbl_balo_table    | balo_thuc_the | active | view_sql=(null)
  tbl_balo_trigger  | balo_thuc_the | active | view_sql=(null)
  tbl_balo_view     | balo_thuc_the | active | view_sql=(null)
đối chứng: 28 report active, 24 cái còn lại collection khác ⇒ không có nút

⇒ Report Balo mới = thêm một dòng table_registry với collection='balo_thuc_the'tự nhận nút. Không sửa code, không hardcode URL/report ID/danh sách 4 loại.


11. ONE-CLICK RESULT

Dry-run ngay trước khi bấm (gate):

BALO_RESULT|add=0|revive=0|metadata_update=0|mark_missing=0|conflict=0|sequence_safe=t
FINGERPRINT|unchanged=PASS
DRY_RUN|PASS|delta=0|write=0

Bấm đúng một lần, Administrator thật, trên /reports/report-balo-table:

nhãn trước  : "Đồng bộ Balo từ PG"
nhãn sau    : "Balo đã mới nhất"                       ← ALREADY_UP_TO_DATE
số dòng bảng: 50 trước → 50 sau
full page reload sau khi bấm : 0                       ← ✅ tự refresh tại chỗ, KHÔNG reload toàn app

HTTP 200
{"status":"ALREADY_UP_TO_DATE","before_total":2148,"after_total":2148,
 "add":0,"revive":0,"metadata_update":0,"mark_missing":0,"conflict":0,
 "verified":true,"message":"Balo đã mới nhất"}

apply không được gọi (delta=0). Không tạo object PG thử để ép nhánh UPDATED — nhánh đó đã có fixture (3C) + atomic apply production (LỆNH 2C) trước đây.


12. SINGLETON RESULT

Giữ lock có kiểm soát, không WRITE (flock -n 9 trên /run/lock/incomex-balo-reconcile-sync.lock), rồi gọi endpoint bằng phiên Administrator:

lock ACQUIRED (mô phỏng sync đang chạy)
→ HTTP 409
  {"status":"ALREADY_RUNNING","before_total":0,"after_total":0,...,"verified":false,
   "message":"Đang có phiên đồng bộ khác"}          ← apply KHÔNG gọi
lock RELEASED
→ HTTP 200
  {"status":"ALREADY_UP_TO_DATE","before_total":2148,"after_total":2148,"verified":true,...}

13. BALO FINGERPRINT / COUNT / DATE_UPDATED

PRE (03:37Z) POST (04:02Z)
COUNT_TOTAL 2148 2148
table / view / function / trigger 385 / 699 / 654 / 410 385 / 699 / 654 / 410
DATE_UPDATED_HASH ae41989817f47ca97cf4a07b3cb89c5d ae41989817f47ca97cf4a07b3cb89c5d
ROWS_MD5 c2cce083b9c5535d48431c4578fa0e2a c2cce083b9c5535d48431c4578fa0e2a

diff PRE POSTrỗng. Runner:

BALO_RESULT|add=0|revive=0|metadata_update=0|mark_missing=0|conflict=0|sequence_safe=t
FINGERPRINT|before = after = 2148|1|2148|8ed1cd5d2f93f5f8a970497bd5e4ec87|2148|t|1
FINGERPRINT|unchanged=PASS
VERIFY|PASS_RECONCILED|delta=0|write=0

delta=0 · conflict=0 · sequence_safe=true · write=0 · fingerprint trước=sau · KEEP date_updated hash trước=sau. Counts bằng live baseline đầu mission (không hardcode).


14. CANONICAL DOCS

knowledge/dev/laws-new/pg-read-pg/balo-thuc-the-quy-dinh.mdrevision 16

⚠️ Phải nói thẳng: §15 của v1.9 ghi nguyên văn "Không tạo … cron, queue, nút UI đồng bộ Balo hoặc verifier framework riêng" — tức canonical đang cấm đúng thứ mission này xây, và cũng cấm "daemon".

Tôi không lặng lẽ bỏ qua. Đã sửa §15 tường minh: bỏ đúng hai mục nút UI đồng bộ Balodaemon khỏi danh sách cấm, kèm ghi chú SỬA ĐỔI nêu rõ đây là Owner chủ động bãi bỏ qua chuỗi lệnh 3A→3D.3 + lệnh 2026-07-17, sau khi thiết kế được khoá thành fixed-purpose bridge; mọi lệnh cấm còn lại của §15 vẫn hiệu lực và không được suy rộng miễn trừ sang cron/queue/wrapper/verifier.

Thêm §16 mới: 16.1 hai nút hai bản chất (Làm mới = ĐỌC / Đồng bộ = WRITE, cấm đổi tên) · 16.2 chuỗi một-lần-bấm, đường ghi vẫn là §0 dot-pg-atomic-apply, Directus/Nuxt không WRITE trực tiếp · 16.3 tự kế thừa, không hardcode · 16.4 điều kiện vận hành bắt buộc · 16.5 không cron, lock 409 · 16.6 bridge fixed-purpose, không nhận command/path, không TCP/public · 16.7 evidence tại thời điểm (không phải quy tắc).

knowledge/dev/laws-new/pg-read-pg/balo-thuc-the-design-handoff.mdrevision 3

Sửa đúng dòng đã lỗi thời ("UI Làm mới chỉ đọc; hiện không có cron") → ghi nhận nút mới + vẫn không có cron, kèm mục vận hành/deploy (socket mount, group_add 987, hai biến role env, exact Nginx route, chỉ docker compose up -d nuxt) và trỏ về §16.

Không sửa tài liệu lịch sử — 7 báo cáo 3A→3D.3 giữ nguyên.


15. LOCAL COMMITS

Repo /opt/incomex (branch feat/s177-sprint1-round-a, không remote, local-only, không push):

# Commit Nội dung Diff
1 4ecb614249d913d42fc5bfed5de5e479af7b9e67 baseline: adopt live nginx + compose as SSOT before Balo route docker/nginx/conf.d/default.conf + docker/docker-compose.yml · 89+/87− (thuần Git, file live không đổi)
2 78b523c3797beb48fc36230702b0bdceb4ced9d5 nginx: exact route /api/balo/reconcile -> Nuxt (Balo one-click sync) docker/nginx/conf.d/default.conf · 12+
3 7a9843d… compose(nuxt): wire Balo bridge socket, group_add 987, operator role ids docker/docker-compose.yml · 16+

Không git add -A, không git push, không trigger workflow. Dùng đường dẫn tường minh cho từng commit.

Lưu ý: working tree còn dirty ở claude-kb/, dot/bin/dot-dot-health, dot/bin/dot-vector-audit, lark-client/, scripts/smoke-test.sh, scripts/test-mcp-connectivity.sh. Đây là dirt có sẵn từ trước, không do mission này — chính vì vậy tôi dùng đường dẫn tường minh thay vì git add -A. Không đụng tới.

⚠️ Cảnh báo bắt buộc: gh trên VPS chưa authenticated (gh auth statusnot logged into any GitHub hosts). Không xác minh được trạng thái disabled của hai workflow deploy cũ (web-test Deploy-to-VPS 233946923, agent-data Vector-Audit 244569412). Không bật, không chạy workflow trong mission này.


16. BACKUP VÀ ROLLBACK COMMAND

Mọi asset rollback là file + image có tagkhông phụ thuộc container stopped (vốn bị docker system prune -f xoá trong ≤60 phút).

/opt/incomex/backups/balo-oneclick-20260717T033701Z/
    default.conf.PRE          3b947f4d…  (68947 B)   ← nginx trước route
    default.conf.POST         (69502 B)              ← nginx sau route
    docker-compose.yml.PRE    7f243336…  (6963 B)
    nginx-T.PRE.txt · nginx-inspect.PRE.json · nuxt-inspect.PRE.json · agent-data-inspect.PRE.json
/opt/incomex/deploys/
    nuxt-output.bak.pre-balo-20260717T033701Z   fp 679ea3de…  ← output production CŨ (byte-exact)
    nuxt-output-balo-fresh-20260717T033701Z     fp 0454f85d…  ← artifact ĐANG chạy
    nuxt-output-balo-20260716T234206Z                         ← candidate 3C/3D.2, giữ làm provenance
rollback image: asia-southeast1-…/web-test/nuxt-ssr:latest  72715a92885c   (CÓ TAG ⇒ prune không xoá)

Rollback Nginx (~5s)

TS=20260717T033701Z
cp -a /opt/incomex/backups/balo-oneclick-$TS/default.conf.PRE \
      /opt/incomex/docker/nginx/conf.d/default.conf
docker exec incomex-nginx nginx -t && docker exec incomex-nginx nginx -s reload

Rollback Nuxt (~30s) — không cần container cũ

TS=20260717T033701Z
cd /opt/incomex/docker
docker compose stop nuxt
rm -rf /opt/incomex/deploys/nuxt-output
cp -a /opt/incomex/deploys/nuxt-output.bak.pre-balo-$TS /opt/incomex/deploys/nuxt-output
cp -a /opt/incomex/backups/balo-oneclick-$TS/docker-compose.yml.PRE \
      /opt/incomex/docker/docker-compose.yml
docker compose up -d nuxt          # ★ CHỈ nuxt

Rollback toàn bộ

Rollback Nuxt → rollback Nginx → git -C /opt/incomex reset --hard b5e0f96 (3 commit của mission).

Rollback dữ liệu: không cần và không được tự làm. Đường ghi duy nhất là dot-pg-atomic-apply; kết quả là ALREADY_UP_TO_DATE (write=0). Nếu fingerprint đổi ⇒ dừng, gọi Owner, không tự rollback DB.


17. PHÉP ĐO PASS §19 — 12/12

# Gate Kết quả
1 Nginx exact route đến Nuxt nginx -T count=1; public trả chữ ký Nuxt
2 Nuxt có socket mount /run/incomex -> /run/incomex rw=false
3 Nuxt có supplementary group GroupAdd=[987]; groups=987,65533
4 Cả private/public role config ✅ 2 biến; nút hiện + endpoint auth đúng
5 Nuxt healthy, Agent Data không recreate ✅ healthy; agent-data ID/StartedAt/Mem/ENV-sha == PRE
6 Administrator thấy nút trên mọi Balo hiện tại ✅ 4/4 (browser thật)
7 Report Balo tương lai tự kế thừa ✅ route động + gate resolvedCollection + table_registry
8 Public endpoint auth/CSRF PASS ✅ 401/403/403/403/403/400/404
9 Một click → ALREADY_UP_TO_DATE + refresh bảng ✅ "Balo đã mới nhất", 0 full reload
10 Singleton → 409 ALREADY_RUNNING, verified=false, apply không gọi
11 Fingerprint/date_updated không đổi khi delta=0 ✅ PRE == POST (4/4 phép đo)
12 Rollback bằng artifact/config bền vững đã ghi ✅ §16, prune-proof

18. NHỮNG ĐIỀU PHẢI NÓI THẲNG

18.1 Lỗi của tôi — rò rỉ secret ra evidence

Khi so sánh compose PRE/POST, tôi chạy docker compose -f <backup>/docker-compose.yml.PRE config — điều này đặt project-directory vào thư mục backup, nên compose không đọc được .env, mọi biến render thành "", và mọi service hiện "CHANGED" giả. Tệ hơn: bản POST render secret dạng plaintext (OpenAI key, DB password, Directus token, Qdrant key) và tôi đã tee nó vào $EV/compose-render-diff.txt.

Xử lý ngay: shred -u cả 3 file (/tmp/cfg.PRE.yml, /tmp/cfg.POST.yml, $EV/compose-render-diff.txt); kiểm lại grep -rlE "sk-proj-|…" $EV0. Làm lại bằng --no-interpolate + --project-directory ⇒ không secret, và kết luận đúng (5 service UNCHANGED, chỉ nuxt CHANGED).

Đánh giá phơi nhiễm: file tồn tại ~40 giây trên /opt/incomex/evidence/ (root-only, không public, không sync ra ngoài), đã shred. Secret không vào git (evidence dir không tracked), không vào KB. Rủi ro thực tế thấp nhưng không phải bằng không — Owner có thể muốn xoay OpenAI/Qdrant key nếu áp dụng chính sách nghiêm ngặt. Tôi không tự xoay (ngoài phạm vi + rủi ro gián đoạn).

Bài học: đây đúng loại lỗi mà forensic của tôi phê phán ở mission khác — dùng phương pháp không có khả năng cấu trúc để trả lời câu hỏi. So compose phải --no-interpolate.

18.2 Một giả định của tôi bị đo thật bác bỏ

Tôi thấy cookie host-only ở directus.* và suy ra endpoint ở vps.* sẽ luôn 401 — nghe rất thuyết phục. Browser thật cho thấy sai: app login qua proxy same-origin xoá Domain, cookie nằm đúng vps.*. Nếu tôi HOLD dựa trên suy luận đó thì đã tạo HOLD thứ tư vô căn cứ.

18.3 Canonical §15 vốn cấm chính thứ này

Đã xử lý tường minh (§14). Nếu Owner không muốn bãi bỏ §15 thì phải nói ngay — nút đang LIVE.

18.4 curl không kiểm được nút

SSR không render nút (do if (import.meta.server) return). Chỉ curl sẽ kết luận sai là FAIL. Phải browser thật. Ghi lại để mission sau không vấp.


19. REMAINING UNKNOWN

# Unknown Blocker?
1 Hai workflow deploy cũ (web-test Deploy-to-VPS 233946923, agent-data Vector-Audit 244569412) còn disabled hay không — gh trên VPS chưa auth, không xác minh được. Disable từ 2026-07-15. Không cho nút. cho an toàn dài hạn: nếu bị bật lại, chúng đắp nginx tháng 3 (nuxt-repo/infra/nginx, sha d753fa0c, mtime 2026-03-30) lên production — đúng cơ chế sự cố 11/07, và sẽ xoá mất route balo. Owner kiểm từ máy có auth.
2 Disk 91% > ngưỡng 85%docker system prune -f chạy mỗi giờ, thu hồi ~0 (91%→91%), xoá mọi container stopped trong ≤60′. Không — rollback §16 prune-proof. Nhưng mọi canary/blue-green tương lai sẽ bị xoá. Không sửa disk-monitor.sh trong mission này (chưa chứng minh nó can thiệp container đang chạy). Đề xuất phiên riêng.
3 GID 987 do OS cấp, ghim cứng compose. Không — preflight getent bắt. Nếu group tạo lại khác GID ⇒ EACCES âm thầm.
4 BrokenPipeError chưa bắt tại dot-balo-reconcile-bridge:167 (sendall); bridge .service không có Restart=. Không — mỹ phẩm/socket-activated. Backlog.
5 Console errors lúc load (401 từ Directus init trước login, vài 404 asset). Không — có sẵn trước mission, không do thay đổi này.

20. CLEANUP

Không xoá (theo §18 lệnh): output production cũ (nuxt-output.bak.pre-balo-*), backup compose, backup Nginx, artifact mới, candidate 3C/3D.2, evidence. Không thay đổi disk-monitor.sh. Đã xoá (bắt buộc, an toàn): file tạm chứa session/credential (/tmp/adm.cookies, /tmp/adm.token, /tmp/vps.cookies, script playwright) và các file render secret (§18.1) — shred -u, kiểm còn sót = 0.

Cleanup thật là mission riêng sau khi Owner xác nhận production.


21. EVIDENCE PATHS

/opt/incomex/evidence/claude-balo-oneclick-20260717T033701Z/
    dryrun.PRE.txt · verify.PRE.txt · balo-baseline.PRE.txt
    nginx-t.txt · route-proof.txt                     ← ★ route tự chứng minh trước Nuxt
    artifact-inventory.txt · fresh-vs-candidate.txt   ← ★ rebuild xác nhận candidate không tamper
    compose-structural-diff.txt                       ← --no-interpolate, không secret
    compose-up-nuxt.txt · deploy-proof.txt            ← ★ agent-data zero-touch
    docker-ps.PRE.txt · agent-data.PRE.txt
    public-verify.txt · button-ui-verify.txt
    button-browser-check.txt                          ← ★ browser thật, nút 4/4
    dryrun.PRECLICK.txt · one-click.txt               ← ★ ALREADY_UP_TO_DATE
    singleton.txt                                     ← ★ 409
    balo-baseline.POST.txt · verify.POST.txt
    inheritance-proof.txt · final-state.txt

/opt/incomex/backups/balo-oneclick-20260717T033701Z/
    default.conf.PRE (3b947f4d…) · default.conf.POST · docker-compose.yml.PRE (7f243336…)
    nginx-T.PRE.txt · nginx-inspect.PRE.json · nuxt-inspect.PRE.json · agent-data-inspect.PRE.json

/opt/incomex/deploys/
    nuxt-output                                  ← LIVE, fp 0454f85d…
    nuxt-output.bak.pre-balo-20260717T033701Z    ← rollback, fp 679ea3de…
    nuxt-output-balo-fresh-20260717T033701Z      ← artifact staged
    nuxt-output-balo-20260716T234206Z            ← candidate 3C/3D.2 (provenance)

Commits (local-only): 4ecb614 · 78b523c · 7a9843d   trên /opt/incomex feat/s177-sprint1-round-a
KB canonical: balo-thuc-the-quy-dinh.md rev16 · balo-thuc-the-design-handoff.md rev3

22. TRẠNG THÁI CUỐI

NGINX_CHANGE=YES (exact route, +12 dòng)     NGINX_RELOAD=YES (PID 1219625 KHÔNG đổi)
NGINX_RESTART=NO                             NUXT_BUILD=YES (pnpm, typecheck+build PASS)
NUXT_RECREATE=YES (chỉ nuxt)                 AGENT_DATA_TOUCH=NO (ID/StartedAt/Mem/ENV == PRE)
OTHER_SERVICE_RECREATE=NO                    COMPOSE_UP_BARE=NO
BALO_WRITE=NO (write=0, ALREADY_UP_TO_DATE)  DB_WRITE=NO
PG_TEST_OBJECT=NO                            CRON_CREATED=NO
GIT_COMMIT=YES (3, local-only)               GITHUB_PUSH=NO (repo không remote)
WORKFLOW_TRIGGER=NO                          BACKUP_DELETED=NO
BRIDGE_TCP=NO                                DOCKER_SOCKET_MOUNT=NO
DOT_REPO_MOUNT=NO                            SSH_KEY_MOUNT=NO

DỪNG SAU BÁO CÁO. Không chuyển sang report "Balo gần đây".