KB-730C

LỆNH 3B — Nút chung Đồng bộ Balo từ PG — Gate 0 HOLD

13 min read Revision 1
codexreportbalonuxtsync-buttongate0hold2026-07-16

LỆNH 3B — Nút chung “Đồng bộ Balo từ PG”

Ngày thực hiện: 2026-07-16
Verdict: HOLD
Mã HOLD: SAFE_EXECUTION_BRIDGE_MISSING + AUTH_PATTERN_UNPROVEN + SINGLETON_LOCK_MISSING
Production: ZERO-TOUCH — chưa deploy, chưa chạy apply

0. Kết luận điều hành

Đã chọn đúng một vị trí chung cho nút:

SharedDirectusTable
→ title bar của bảng
→ đặt cạnh nút “Làm mới” hiện có
→ chỉ hiện khi resolvedCollection === 'balo_thuc_the'

Vị trí này tự bao phủ bốn report Balo hiện tại và mọi report Balo tương lai dùng cùng collection, không hardcode URL. Tuy nhiên Gate 0 phát hiện ba điều kiện an toàn bắt buộc chưa tồn tại:

  1. Nuxt không nhìn thấy dot-balo-reconcile; execution bridge hiện hữu chỉ plan-only và từ chối REAL_RUN.
  2. Hệ thống chưa có pattern server-side đã chứng minh cho user session + role Owner/operator + same-origin/CSRF.
  3. dot-balo-reconciledot-pg-atomic-apply chưa có singleton lock.

Theo Lệnh 3B, đã không tạo endpoint, không tạo nút mock, không sửa DOT, không build một candidate nửa an toàn. Làm UI/endpoint trước ba gate này có thể tạo cảm giác “sẵn sàng deploy” sai sự thật.

DOT hiện có đã đủ preflight, atomic apply, post-verify và marker máy đọc. Không cần sửa logic reconcile hoặc output ở bước này.

1. Luật, tuyên ngôn và Assembly Gate

Đã đọc trực tiếp:

  • .claude/skills/incomex-rules.md — 36 mục, 8 bước và quy trình hai mũ;
  • Operating Rules SSOT v7.58 rev51;
  • VPS Operating Rules v1.0 rev2;
  • Hiến pháp v4.6.3 rev44;
  • Quy định Balo canonical v1.9 rev15;
  • báo cáo Lệnh 2D rev2.

Ba câu tuyên ngôn:

  1. Vĩnh viễn: một vị trí theo collection balo_thuc_the, một endpoint cố định và một DOT canonical; report Balo tương lai tự kế thừa mà không sửa URL/code từng trang.
  2. Nhầm được không: chỉ đạt khi có auth server-side, same-origin/CSRF, bridge fixed-function và singleton lock. Hiện ba lớp này chưa đủ, nên HOLD là rào chắn đúng.
  3. Tự động: sau thao tác có thẩm quyền, workflow phải tự chạy dry-run → gate → apply → verify → refresh bảng; hiện chưa thể đóng kín vì thiếu bridge và auth đã chứng minh.

Assembly Gate:

PG: public.balo_thuc_the + live pg_catalog đã có
Directus: collection balo_thuc_the và table_registry đã có
DOT: dot-balo-reconcile đã PASS production
Nuxt dynamic: SharedDirectusTable đã resolve collection từ table_registry
Điều kiện thiếu: execution boundary + auth/role + singleton lock

2. Gate 0 — nền source và runtime

Nuxt source SSOT:

path=/opt/incomex/docker/nuxt-repo
HEAD=ae694716d5647ce1ed7f3118274645f5d2cf060b
branch=main
status:
 ? web-rp-current

Không có diff Nuxt do Lệnh 3B. web-rp-current là linked worktree có sẵn, không thuộc mission.

Production Nuxt:

name=/incomex-nuxt
image=asia-southeast1-docker.pkg.dev/github-chatgpt-ggcloud/web-test/nuxt-ssr:latest
started=2026-07-11T02:50:33.302411141Z
restart=0
running=true
mount=/opt/incomex/deploys/nuxt-output -> /app/.output rw=true
NUXT_DOT_VISIBLE=no

Nuxt không mount /opt/incomex, không có Docker socket, không có SSH key và không nhìn thấy DOT. Đây là trạng thái an toàn hiện tại.

3. Gate 0 — execution bridge

3.1 Executor hiện có không phải bridge WRITE

Service đã kiểm:

name=/incomex-agent-api-executor
image=agent-api-executor-local:v1
started=2026-06-04T08:46:26.827398196Z
restart=0
running=true
user=10001
network=docker_incomex
port=127.0.0.1:8090
mounts=(none)
DOT_VISIBLE=no

OpenAPI chỉ có:

/healthz   GET
/selfcheck GET
/dispatch  POST

Source contract của service ghi rõ:

agent_api plan-only executor
this service NEVER writes to PostgreSQL
REAL_RUN refused
execute_enabled=false
real_run_enabled=false
dry_run_only=true

Service chỉ có fixture dot:kg:explain; không có dot-balo-reconcile, không có Docker access và không thể gọi host DOT. Vì vậy không được tái sử dụng bằng cách đổi nghĩa service plan-only thành cổng WRITE.

3.2 Không có bridge khác đủ điều kiện

Inventory mount của các container xác nhận:

  • Nuxt chỉ mount production .output;
  • Agent API Executor không có mount;
  • Agent Data không có DOT/Docker socket;
  • Claude MCP có Docker socket và một số thư mục DOT read-only, nhưng đó là admin/MCP boundary, không phải fixed-function browser bridge và Lệnh 3B cấm browser gọi trực tiếp gateway/admin tool.

Không có container/service nào đồng thời đáp ứng:

Nuxt gọi được
+ chỉ cho đúng dot-balo-reconcile
+ không nhận command/path
+ có quyền chạy DOT host
+ có lock
+ trả output đã lọc

Kết luận:

HOLD: SAFE_EXECUTION_BRIDGE_MISSING

4. Gate 0 — auth và same-origin/CSRF

Các auth pattern hiện có:

  1. Directus auth middleware đọc useDirectusAuth().user, nhưng plugin có:
if (import.meta.server) return
enableGlobalAuthMiddleware=false
  1. Firebase middleware có:
if (process.server) return
  1. Directus proxy hiện có khả năng chuyển tiếp cookie directus_session_token/__session, nhưng không kiểm role cho server action.
  2. Các POST/PATCH action hiện có không cung cấp một pattern hoàn chỉnh user-session + role + same-origin/CSRF để tái sử dụng. Bearer token ở AI search là service token riêng, không phải user Owner/operator session.

Role Directus đọc-live hiện có gồm Administrator, tac-admin, Editor, AI Agent, Agent, tac-agent và các role Public. Không có role tên rõ ràng Owner hoặc Balo operator; chưa có quyết định role nào được phép vận hành Balo.

Do ẩn nút client không thay thế authorization server-side, không được tự suy luận Administrator hay tac-admin là quyền Owner.

Kết luận:

HOLD: AUTH_PATTERN_UNPROVEN

5. Gate 0 — singleton lock

Đã đọc toàn bộ hai file:

/opt/incomex/dot/bin/dot-balo-reconcile
/opt/incomex/dot/bin/dot-pg-atomic-apply

Không tìm thấy:

flock
lockfile
pg_try_advisory_lock
ALREADY_RUNNING
singleton guard

UI disabled chỉ chặn một browser; hai browser vẫn có thể đi vào apply đồng thời. Atomic transaction không thay thế singleton workflow lock.

Kết luận:

HOLD: SINGLETON_LOCK_MISSING

6. DOT contract đã đủ — read-only evidence

DOT canonical:

path=/opt/incomex/dot/bin/dot-balo-reconcile
sha256=7e657b96cc27e0f396e2485a67d6296c4de10aabd2f68cf4aaf367cb7b9bdfe5
DOT_HEAD=bddfdcc5a504ff6f4c1bc4fdf075dc8dde209402

Dry-run production chỉ đọc:

SQL_LOCK|manifest=PASS|sha256=c018f12ce86918c2a6199b70c4626c981deb2632edc686c1544504a3cf387369|atomic_guard=PASS_NONEXECUTING
table|385|385|0|0|0|0|0
view|699|699|0|0|0|0|0
function|654|654|0|0|0|0|0
trigger|410|410|0|0|0|0|0
TOTAL|2148|2148|0|0|0|0|0
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

Verify production chỉ đọc:

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

Các marker đã đủ để parser fail-closed. Với delta hiện tại, contract tương lai phải trả ALREADY_UP_TO_DATE và tuyệt đối không gọi --apply.

7. Vị trí nút đã chọn và cơ chế tự kế thừa

Vị trí duy nhất:

web/components/shared/DirectusTable.vue
title bar, cạnh nút “Làm mới” hiện có

Lý do:

  • dynamic report host chỉ truyền tableId;
  • SharedDirectusTable đọc table_registry rồi tính resolvedCollection;
  • handleRefresh() đã refresh đúng bảng hiện tại và emit refreshed;
  • không cần sửa từng URL/report.

Điều kiện hiển thị thiết kế:

isBaloCollection = resolvedCollection === 'balo_thuc_the'
show = isBaloCollection && currentUserHasApprovedBaloOperatorRole

Role check ở client chỉ phục vụ UX; endpoint vẫn bắt buộc kiểm server-side.

Registry live chứng minh bốn report hiện tại cùng collection:

tbl_balo_function|/reports/report-balo-function|balo_thuc_the|active|view_sql=null
tbl_balo_table|/reports/report-balo-table|balo_thuc_the|active|view_sql=null
tbl_balo_trigger|/reports/report-balo-trigger|balo_thuc_the|active|view_sql=null
tbl_balo_view|/reports/report-balo-view|balo_thuc_the|active|view_sql=null

Vì nhận diện theo collection:

  • bốn report trên tự có nút;
  • report Balo tương lai dùng balo_thuc_the tự có nút;
  • PG Census có view_sql và collection khác nên không có nút;
  • report không phải Balo không có nút.

Không cần cột/flag DB mới và không hardcode bốn URL.

8. Đề xuất đúng một bridge tối thiểu cho mission riêng

Đề xuất duy nhất:

Fixed-purpose host bridge qua Unix socket riêng
/run/incomex/balo-reconcile.sock

Contract bắt buộc của bridge:

  1. Chỉ một action POST /reconcile, body rỗng; không nhận command/path/mode/SQL/container/user/database.
  2. Chạy trên host, gọi đúng /opt/incomex/dot/bin/dot-balo-reconcile; không mount Docker socket, SSH key hoặc /opt/incomex vào Nuxt.
  3. Unix socket chỉ bind-mount đúng socket vào Nuxt; không mở TCP/public port.
  4. Khóa tại execution boundary bằng:
flock -n /run/lock/incomex-balo-reconcile.lock

Phiên thứ hai trả 409 ALREADY_RUNNING. 5. Workflow cố định: dry-run → parse gate → delta=0 thì dừng → apply khi an toàn → verify delta=0. 6. Parser chỉ chấp nhận marker/field whitelist; output sai contract trả HOLD/FAILED; không trả raw shell, env, command hay stack trace. 7. Service/unit được harden và không cung cấp shell/API tổng quát.

Đây là đề xuất kiến trúc, chưa tạo và chưa deploy. Không mở rộng executor plan-only hiện tại.

9. Endpoint/UI contract sau khi bridge và auth PASS

Sau gate riêng, candidate mới được phép tạo:

POST /api/balo/reconcile
BaloSyncButton.vue

Endpoint:

  • xác thực Directus session server-side;
  • kiểm role ID đã được Owner phê duyệt;
  • kiểm same-origin/CSRF theo pattern đã duyệt;
  • gọi duy nhất Unix-socket bridge;
  • request body phải rỗng;
  • response chỉ chứa schema đã cho trong Lệnh 3B.

UI:

  • component nhỏ cạnh Làm mới;
  • disabled khi đang chạy, không retry WRITE;
  • chỉ UPDATEDverified=true mới gọi handleRefresh();
  • ALREADY_UP_TO_DATE không apply và có thể refresh read-only;
  • HOLD/FAILED không refresh giả thành công.

10. Test/build/Git

Không có code candidate vì Gate 0 HOLD trước Bước 3. Do đó:

ENDPOINT_CREATED=NO
UI_COMPONENT_CREATED=NO
TYPECHECK=N/A_NO_CODE_AFTER_HOLD
BUILD=N/A_NO_CODE_AFTER_HOLD
MOCK_TEST=N/A_NO_ENDPOINT_AFTER_HOLD
NUXT_COMMIT=NONE
DOT_COMMIT=NONE

Nuxt mission diff rỗng; HEAD giữ nguyên:

ae694716d5647ce1ed7f3118274645f5d2cf060b

Không tạo commit no-op.

11. Zero-touch production

NUXT_DEPLOY=NOT_RUN
BALO_APPLY_FROM_UI=NOT_RUN
DOT_APPLY=NOT_RUN
DB_WRITE=NOT_RUN
CONTAINER_RESTART_RELOAD_RECREATE=NOT_RUN
NGINX_CHANGE=NOT_RUN
COMPOSE_CHANGE=NOT_RUN
CRON_CHANGE=NOT_RUN
GITHUB_PUSH=NOT_RUN
WORKFLOW_TRIGGER=NOT_RUN

Container cuối mission:

nuxt_started=2026-07-11T02:50:33.302411141Z restart=0 running=true
executor_started=2026-06-04T08:46:26.827398196Z restart=0 running=true

Hai lệnh đã chạy với DOT đều là --dry-run--verify; output xác nhận write=0 và fingerprint không đổi.

12. Nợ/gate cần Owner quyết

TD-3B-01: phê duyệt mission riêng xây fixed-purpose Unix-socket bridge.
TD-3B-02: chỉ định chính xác Directus role ID được phép vận hành Balo
           (không tự suy luận Administrator hay tac-admin).
TD-3B-03: trong mission bridge, chứng minh singleton 409 và auth/CSRF trước khi quay lại code UI.

OR v7.58 không cần cập nhật: không phát hiện nguyên tắc vận hành tổng quát mới; ba blocker là áp dụng trực tiếp fail-closed/security gate hiện hành. Canonical Balo chưa sửa vì Lệnh 3B dừng trước thay đổi sản phẩm/runtime.

13. Evidence paths

Nuxt source: /opt/incomex/docker/nuxt-repo
DOT source: /opt/incomex/dot/bin/dot-balo-reconcile
Atomic gate: /opt/incomex/dot/bin/dot-pg-atomic-apply
Executor runtime source: /app/main.py trong incomex-agent-api-executor
Report: knowledge/current-state/reports/codex-lenh-3b-balo-sync-button-2026-07-16.md

CHỐT: HOLD. Đã chọn đúng vị trí nút, nhưng không được code/deploy cho tới khi bridge, auth và singleton lock PASS trong gate riêng.