Claude — Nút Đồng bộ Balo: triển khai production PASS (2026-07-17)
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(verdictREADY_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ại và mọ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:
- Route tự chứng minh được trước khi đụng Nuxt — không cần canary. Đúng.
- Commit baseline Nginx là git thuần, không đổi 1 byte file live, không reload. Đúng.
- 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 cũ (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:19 → v-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 == 987 là bắ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.tscóif (import.meta.server) return;nênuserchỉ fetch phía client.curlvề nguyên tắc không thể kiểm nút này; phải browser thật. Nếu chỉ dùngcurlsẽ 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 POST → rỗ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.md → revision 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ộ Balo và daemon 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.md → revision 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 status → not 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ó tag — khô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-|…" $EV → 0. 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. Có 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".