KB-5BE0 rev 14

Nâng cấp đầu nối MCP "Incomex VPS": bộ fs_* 3 gốc + vps_status + ui_screenshot + trợ lý host (2026-09-17)

77 min read Revision 14
mcpclaude-mcpincomex-vpsfs-rootshost-helperreport2026-09-17

Nâng cấp đầu nối MCP "Incomex VPS" — báo cáo nghiệm thu (2026-09-17)

Người làm: Claude Code (Fable 5.1). Mọi dòng "ĐẠT" dưới đây đều đo bằng lời gọi MCP thật qua đầu nối claude.ai → nginx → container, làm trên bản sao, không đụng bản gốc.

1. Bảng kết quả (công cụ × gốc)

Công cụ gh (GitHub) ui (xưởng vẽ) docs (chỉ đọc)
fs_list ✅ ĐẠT (kèm git HEAD, tự pull) ✅ ĐẠT ✅ ĐẠT
fs_read theo đoạn ✅ ĐẠT — file 314KB, có dòng dài 238.270 ký tự, cắt theo ký tự + con trỏ đọc tiếp ✅ ĐẠT — đọc JS dòng 1500 (sau byte 51.200) ✅ ĐẠT
fs_search ✅ ĐẠT ✅ ĐẠT ✅ ĐẠT
fs_edit ✅ ĐẠT — commit 97f71b3 hiện trên GitHub, diff đúng 1 dòng; old_str trùng 6 chỗ → từ chối kèm số dòng; Mac chen ngang + phiên bản cũ → từ chối, thay đổi của Mac còn nguyên ✅ ĐẠT — bản cũ commit 08bf162 rồi bản sửa 819f6e5 ✅ bị từ chối đúng
fs_write ✅ ĐẠT (tạo mới + push) (dùng chung mã, không thử riêng) ✅ bị từ chối đúng
fs_log / fs_diff ✅ ĐẠT ✅ ĐẠT — (không có sổ git: báo rõ)
ui_screenshot ✅ ĐẠT — ảnh trước/sau khác rõ (nền xám → vàng)
vps_status ✅ 5/5 mục trả kết quả (containers · systemd · resources · logs · git); muc="containers; id"service="incomex-nginx; cat /etc/shadow"bị từ chối
8 công cụ cũ read_file ✅ · query_pg ✅ · write_file ✅ · pg_schema ❌ và directus_ ❌ — HỎNG TỪ TRƯỚC, không do đợt này* (xem ghi nợ N1, N2)

Thử phá script root: 8 ca docker-logs (x; id, $(id), --since, tên ngoài danh sách, n=0/99999/10; id, lệnh con lạ) + 6 ca ui-commit (ngoài gốc, tuyệt đối, .git/config, symlink ra ngoài, qua symlink ui-preview, thừa trường) → 14/14 bị từ chối, mỗi ca có 1 dòng journald. sudo lệnh khác bằng user trợ lý → "a password is required". Message commit có xuống dòng + $(id) vào commit nguyên văn như chữ.

2. Địa chỉ

  • Repo: https://github.com/Huyen1974/incomex-workspace (PRIVATE, nhánh main). Deploy key ghi 163589117, gắn duy nhất repo này.
  • Gốc gh = /opt/incomex/mcp-roots/gh (chỉ user trợ lý ghi; container mount chỉ đọc).
  • Gốc ui = /opt/incomex/docs/mcp-writes/đây là thư mục thật phục vụ /ui-preview/mcp-writes/. mcp-writes/ui-preview chỉ là symlink trỏ về . (từ 27/07) nên hai tài liệu cũ không lệch nhau. Đã có sổ git (/var/lib/incomex/ui-preview-notebook.git, snapshot 5 phút).
  • Gốc docs = /opt/incomex/docs + /opt/incomex/dot/specs (path bắt đầu bằng docs/ hoặc specs/).
  • Nhật ký ghi duy nhất: /var/log/mcp-audit/fs-writes.jsonl. Mã: /opt/incomex/claude-mcp/app/{fsroots,hostq,hosttools}.py + helper/. Git VPS: commit 72a3f0d.

3. Kiến trúc đã chọn (chủ duyệt 3 lần trong phiên)

Container claude-mcp nhận lệnh từ AI nên giữ nguyên bị khoá: không internet, không git, không docker.sock, không khoá GitHub, không quyền GSM. Phần cần máy chủ do đúng 1 unit incomex-mcp-helper.service làm: user thường (uid 999), nhận việc qua thư mục hàng đợi (không mở cổng), thực đơn cố định 8 việc, không bao giờ chạy chuỗi lấy từ yêu cầu. Đường lên root duy nhất: 1 file sudoers → 1 script /usr/local/sbin/incomex-mcp-helper-priv (3 lệnh con: docker-ps · docker-logs · ui-commit). Đánh đổi: vì sudo cần setuid nên unit không bật được NoNewPrivileges. Lớp bù: ProtectSystem=strict, ProtectHome, PrivateTmp, ReadWritePaths hẹp (4 đường dẫn), script root tự kiểm mọi tham số, sudo -l chỉ hiện đúng 1 dòng. Cách gỡ sạch (chỉ mô tả, chưa làm): xoá /etc/sudoers.d/incomex-mcp-helper + /usr/local/sbin/incomex-mcp-helper-priv. Chụp ảnh: dùng lại Chromium headless Playwright đã có sẵn trên host (chép 260MB sang /usr/local/lib/incomex-mcp-helper/), chỉ cho vps.incomexsaigoncorp.vn, chặn mọi tên miền khác ở tầng DNS của trình duyệt, PNG ≤1,5MB, ≤30s.

4. Khoá bí mật

  • Tên secret: MCP_WORKSPACE_GH_DEPLOY_KEY (GSM, project github-chatgpt-ggcloud). Sinh trong tmpfs trên VPS, lưu thẳng GSM, khoá riêng chưa từng rời VPS/GSM, không có bản trên Mac.
  • Nơi dùng: /usr/local/sbin/incomex-mcp-helper-key-fetch (quyền root, ExecStartPre=+ — cùng cơ chế hermes-key-fetch) → tmpfs /run/incomex-mcp-helper/gh_deploy_key (root:incomex-mcp-helper 0640). User trợ lý không có credential gcloud nào.
  • Xoay khoá (3 dòng): sinh cặp khoá mới → gcloud secrets versions add MCP_WORKSPACE_GH_DEPLOY_KEY + gh repo deploy-key add/deletesystemctl restart incomex-mcp-helper.
  • Tự kiểm cuối ĐẠT: xoá bản nạp tạm (shred) → quét đĩa không còn bản sao → restart → nạp lại từ GSM (hash khớp) → push d56a6a2 thành công.
  • Token nhịp tim Kuma: SSOT là kuma.db (đúng luật sẵn có), cũng nạp vào tmpfs. Đã thêm dòng sổ kê vào BAN-DO-HE-THONG-20260729.md §12.2.

5. Giám sát · đĩa · drift

  • Uptime-Kuma: giữ "MCP Endpoint"; thêm monitor push "MCP Host Helper" (id 18, nhịp 60s, ngưỡng 180s). Đo thật: dừng trợ lý → Kuma ghi DOWN 10:49:03 UTC → chạy lại → UP.
  • Đĩa: trước 15.444MB trống → sau 14.727MB (build image chỉ ~1MB; phần lớn là 260MB Chromium + hoạt động khác của máy). Không prune gì. Ứng viên dọn (chỉ liệt kê): 41GB image Docker "reclaimable", image treo d8ed7813 (bản build đầu), /root/mcp-helper-stage/, các tệp yêu cầu JSON trong queue/done|out (rất nhỏ), image cũ phase1a/rollback* của claude-mcp (KHÔNG đụng rollback-pre-fs-20260917).
  • incomex-config-drift-check: đã DRIFT từ 08/08/2026 vì 2 mục nginx-default-confenv-docker-stack. Sau đợt này thêm mục thứ 3 mcp-compose (do sửa compose claude-mcp — đúng dự kiến). Không tìm thấy quy trình chính thức cập nhật mốc so sánh ⇒ không sửa, không tắt, chỉ báo cáo.

6. Cách quay lui

  1. Container: sửa image: trong /opt/incomex/claude-mcp/docker-compose.claude-mcp.yml về claude-mcp-local:rollback-pre-fs-20260917 (hoặc chép lại …yml.bak-fsroots-20260917T103553Z) → docker compose -f … up -d. Biến môi trường/mount mới vô hại với bản cũ.
  2. Trợ lý: systemctl disable --now incomex-mcp-helper.service (8 công cụ cũ + fs_list/read/search vẫn chạy).

7. Hiện trạng các máy chủ MCP (Bước 0)

Máy chủ Công cụ Giới hạn Trùng nhau
claude-mcp ("Incomex VPS") 8 cũ + 9 mới 50KB/lần, 10s (ghi: 120s)
claude-kb ("Incomex KB") 6, chỉ đọc proxy của agent-data; các công cụ ghi bị khoá có chủ đích 26/07 (path secret cũ bị lộ, chờ xoay) Trùng 100% phần đọc với agent-data
agent-data 10 (đọc + ghi KB) MCP tự viết, không SSE nên Claude chat không gắn thẳng được như trên
cowork-mcp + runner 9 (shell hộp cát, preview, patch) 1 job/lần
Lark gateway ~41 systemd, quyền root

Đề xuất (KHÔNG làm): giữ cặp KB/agent-data như hiện tại cho tới khi xoay path secret của claude-kb; sau đó mở lại công cụ ghi ở claude-kb thay vì thêm đầu nối thứ ba.

8. Danh sách ghi nợ

  • N1 — pg_schema chưa từng chạy được: 205 lần gọi, 0 thành công (lỗi AmbiguousParameter trong mã cũ). Không sửa vì luật "không đổi 8 công cụ cũ". Sửa = ép kiểu %(schema)s::text.
  • N2 — directus_* hỏng từ ≤11/09: container Directus không còn trên mạng claude_mcp_net (lần thành công cuối 18/05). Sửa = chạy scripts/ensure-claude-mcp-network.sh — ngoài phạm vi đợt này.
  • N3 — ĐÃ ĐÓNG 17/09: chủ quyết chuyển repo PUBLIC (trufflehog + regex quét 15 commit/toàn lịch sử: 0 secret) → bật secret scanning + push protection, bảo vệ main chặn force-push + chặn xoá (enforce_admins); deploy key trợ lý host push 6be0053 sau đổi visibility: ĐẠT.
  • N4 — full_page chỉ chụp cao tối đa 5000px (giới hạn của trình duyệt dòng lệnh).
  • N5 — drift-check thêm mục mcp-compose (xem §5).
  • N6 — claude-mcp/README.md còn ghi "3 tools" (tài liệu cũ, không sửa vì luật không tạo/sửa tài liệu khác).
  • N7 — trong lúc làm có bên khác khởi động lại incomex-nuxtincomex-agent-data (không phải đợt này).

9. Ba câu tuyên ngôn

  1. Vĩnh viễn? Gốc/repo/danh sách cho phép đều ở biến môi trường + file cấu hình root; thêm gốc mới = thêm 1 mục JSON, không sửa mã. Khoá mất thì nạp lại từ GSM.
  2. Nhầm được không? Hạ tầng chặn: container không có mạng/khoá/git; trợ lý chỉ có thực đơn; sudoers 1 dòng; edit đòi khớp đúng 1 chỗ + mã phiên bản; không có công cụ xoá.
  3. 100% tự động? Unit tự khởi động cùng máy, tự nạp khoá, Kuma kêu khi chết. Bước tay duy nhất còn lại: người dùng bấm "làm mới công cụ" ở đầu nối claude.ai nếu chưa thấy 9 công cụ mới.

10. ĐỢT 2 — 2026-09-18: gỡ mọi trần tự đặt (phase0.7 "nolimit")

Người làm: Claude Code (Fable 5.1). Chỉ đụng claude-mcp + trợ lý host của nó; không đụng agent-data / workspace GPT. Git VPS: commit e289226. Image claude-mcp-local:phase0.7-nolimit (Dockerfile.phase07 = app/ chồng lên phase0.5a, không cài gì). Lùi: image claude-mcp-local:rollback-pre-nolimit-20260918 (= phase0.6-fs) + docker-compose.claude-mcp.yml.bak-nolimit-20260917T233011Zcompose up -d; trợ lý: bản cũ của 4 file (helper.env, priv.conf, incomex-mcp-helper-priv, mcp_host_helper.py) ở /root/mcp-helper-rollback-20260918/ → chép lại + systemctl restart incomex-mcp-helper. Đĩa: 86% trước và sau (build ~1MB; test để lại 1 file 20MB — xem §10.5).

10.1 Nguyên tắc đã áp dụng

  • Không còn con số trần "tiện tay" trong mã. Mỗi trần là (a) tham số người gọi chọn, mặc định vừa phải để tiết kiệm token, hoặc (b) trần cứng lấy từ biến môi trường (compose cho container, helper.env/priv.conf cho trợ lý), lý do ghi ngay cạnh biến (app/limits.py, compose, helper.env).
  • Bị cắt thì đầu ra luôn nói cắt ở đâu + gọi tiếp thế nào (ĐỌC TIẾP: start_line=…, CÒN NỮA: offset=…, "next": "…offset=5000", chụp tiếp bằng offset_y=…).
  • Giữ nguyên hàng rào: không công cụ xoá (fs_move = git mv, từ chối nếu đích tồn tại), không shell tự do (vps_status vẫn thực đơn cố định; since sang trợ lý dưới dạng số epoch, contains mã hoá hex và so khớp chữ nguyên văn trong Python, không bao giờ thành đối số grep/regex), không force-push, che/chặn bí mật (bộ lọc strict mới cho gốc code), khoá đường dẫn trong gốc, ảnh chỉ vps.incomexsaigoncorp.vn, DB chỉ đọc (READ ONLY + AST + context_pack_readonly), khoá vẫn từ GSM.

10.2 Bảng trần cũ → trần mới → lý do trần mới

Công cụ Trần cũ (gõ cứng) Trần mới Lý do trần mới
fs_read 40KB/lần, max_lines≤2000, file ≤5MB max_bytes tham số (mặc định 40KB) — trần env MCP_TEXT_MAX_BYTES_CAP=1MB; max_lines mặc định 0 = không giới hạn dòng; file nạp vào bộ nhớ FS_FILE_LOAD_MAX_BYTES=64MB 1MB chữ ≈ 300k token — không ngữ cảnh mô hình nào chứa hơn trong 1 câu trả lời. 64MB file ×3 bản (bytes/str/dòng) ≈ 200MB trong container 512MB; GitHub tự giới hạn 100MB/file
fs_diff ~36KB, dòng cắt 300 ký tự max_bytes (mặc định 36KB, trần 1MB) + offset (dòng của bản vá) + line_chars (0 = không cắt) như trên
fs_search max_results≤200, context ≤3, bỏ qua file >2MB, 7s cứng, 5 khớp/dòng max_results trần FS_SEARCH_MAX_RESULTS_CAP=10000 + offset; context tới 50; quét file tới 64MB (cùng trần nạp); time_budget_s tham số (mặc định 7) trần 60s; mọi khớp trên 1 dòng đều báo (file nén); line_chars; max_bytes 60s < proxy_read_timeout 300s của nginx và < wall-clock công cụ; cây /opt/incomex 80k file quét không hết 30s (báo dừng sớm đúng chỗ)
fs_edit ≤20 chỗ sửa không giới hạn số chỗ sửa — chỉ còn kích thước yêu cầu đo thật 50 chỗ/1 lần, 1 commit
fs_write 1MiB; từ chối ghi đè file >50KB; gốc ui đòi thư mục cha có sẵn FS_WRITE_MAX_BYTES=20MB; ghi đè file >FS_OVERWRITE_GUARD_BYTES(50KB) được khi có ĐỦ expected_version + overwrite_large=true; tự tạo thư mục cha ở mọi gốc nội dung đi trong JSON, chữ có dấu bị escape ×2 phải lọt client_max_body_size 50m của nginx ⇒ 20MB; trợ lý nhận payload 25MB (MCP_HELPER_PAYLOAD_MAX_BYTES); mem_limit container 256m→512m (host 12GB)
fs_list depth 1–3, 300 mục, quét tối đa 5000 depth tự do (0 = cả cây), limit trần FS_LIST_MAX_ENTRIES_CAP=5000 + offset, quét lười theo FS_LIST_TIME_S=20s quét theo ngân sách thời gian thay vì đếm mục
fs_log n≤50 n trần FS_LOG_MAX_N_CAP=1000 + offset + follow (theo file qua đổi tên) + max_bytes trợ lý cắt theo hàng, offset luôn chính xác
fs_move (MỚI) git mv gốc gh (pull→mv→commit→push, push bị từ chối thì khôi phục clone) và ui (commit bản hiện tại → đổi tên → ghi nhận vào sổ); từ chối nếu đích tồn tại không phải xoá; lịch sử đi theo file
gốc code (MỚI) /opt/incomex mount chỉ đọc tại /mnt/code; redact: strict; exclude = backups, lark-backups, secrets, data, lark-snapshots (kho dump DB/Lark chứa băm mật khẩu + toàn bộ bản ghi; kho bí mật; dữ liệu cá nhân học viên) — sửa 1 dòng JSON trong compose là đổi uid 10001 chỉ thấy file o+r; xem §10.3 về bộ lọc
ui_screenshot rộng ≤1920, cao ≤2000, full_page ≤5000px, ảnh ≤1,5MB, 30s rộng ≤3840, cao ≤4320, full_page ≤30000px css, ảnh ≤8MB (env SCREENSHOT_* cả 2 phía), 90s; thêm selector, wait_ms, device_scale_factor 0.5–4, offset_y, allow_oversize trình duyệt điều khiển bằng CDP qua --remote-debugging-pipe (không mở cổng, không thư viện). 🔴 Giới hạn thật của hạ tầng là Anthropic API: ≤8000px/cạnh, ≤5MB/ảnh — vượt là ứng dụng chat có thể từ chối cả tin nhắn ⇒ mặc định cắt lát ≤8000px và trả offset_y để chụp tiếp; allow_oversize=true mới chụp 1 ảnh tới 30000px (đo thật 2560×15046px OK)
vps_status logs n≤200 n trần STATUS_LOG_MAX_N_CAP=5000; thêm since ('90s'/'15m'/'2h'/'3d' hoặc mốc UTC), contains (chữ nguyên văn); max_bytes (cắt đầu, giữ cuối) script root: docker-logs <tên> <n> [<epoch> <hex>]; có contains thì cửa sổ quét chặn ở log_scan_lines=200000 dòng để 1 lần gọi không stream cả log nhiều GB; đầu ra script root 2MB (out_cap_bytes, priv.conf root sở hữu)
query_pg LIMIT 500 cứng, 5s cứng, 50KB limit (mặc định 500) trần QUERY_PG_MAX_ROWS_CAP=50000 + offset; timeout_s (mặc định 5) trần 60 (SET LOCAL statement_timeout); max_bytes (mặc định 50KB) trần 8MB; con trỏ phía server → dừng đúng ranh giới hàng, JSON luôn hợp lệ, mỗi hàng 1 dòng (~30% ít token hơn indent) 60s < nginx 300s; role chỉ-đọc dùng chung không giữ snapshot hàng phút; 8MB = 1 thông điệp MCP ×4 bản trong RAM
pg_schema chưa từng chạy (AmbiguousParameter) ép kiểu %(schema)s::text — chạy; max_bytes cắt theo cả bảng, liệt kê tên bảng bị bỏ N1 đóng
directus_read limit ≤500 limit trần DIRECTUS_MAX_ITEMS_CAP=50000 + offset + fields; trả total (meta=filter_count) + next; timeout HTTP 60s chưa đo được — Directus vẫn ngoài mạng (§10.4)
read_file 50KB / 10.000 dòng bỏ trần dòng; max_bytes cùng trần 1MB của fs_read; cắt trên ranh giới dòng (hoặc start_char nếu 1 dòng dài) dùng chung MCP_TEXT_MAX_BYTES_CAP
write_file 1MiB cùng FS_WRITE_MAX_BYTES=20MB
message commit 500/600 ký tự FS_MESSAGE_MAX_CHARS=5000 (cả 3 lớp) file yêu cầu hàng đợi 64KB
trần ngoài cùng mọi câu trả lời 50KB MCP_RESPONSE_HARD_MAX_BYTES=8MB (lưới an toàn; mỗi công cụ tự cắt sạch trước) 1 thông điệp JSON-RPC ×~4 bản trong RAM của container 512MB

Wall-clock từng công cụ (TOOL_TIMEOUTS) đặt theo tham số lớn nhất người gọi có thể xin + biên (query_pg 80s, fs_search 80s, fs_edit/write/move 180s, screenshot 120s).

10.3 Gốc code — phát hiện ngoài dự kiến và cách xử lý

Khi đo bộ lọc trên giá trị bí mật thật (kịch bản /root/mcp-nolimit-stage/leakscan2.py: gom giá trị từ các file .env/secrets/ thật, đẩy mọi file mà gốc code sẽ phục vụ qua đúng hàm che, chỉ báo số đếm + vân tay băm, không in giá trị):

  • 🔴 3 bí mật đang sống nằm gõ cứng trong file ai-cũng-đọc-được dưới /opt/incomex: AGENT_DATA_API_KEY (UUID) nằm ngay trong docker/nginx/conf.d/default.conf (+13 bản .bak của nó, scripts/reconcile-*.py, docker/nuxt-repo/automation/config.js, docker/agent-data-repo/scripts/*); INCOMEX_PG_PASSWORD trong docker/agent-data-repo/scripts/import_firestore_to_pg.py + .github/workflows/s109-pg-setup.yml; MYSQL_ROOT_PASSWORD trong 2 báo cáo W205_fix_db_auth_safe_password.md. Bộ lọc cũ (sanitize.redact) không che được chúng (đo: 32 cặp file×bí-mật sống sót).
  • Đã thêm chế độ redact_strict (chỉ gốc code): che URL path-secret, mọi phép gán có tên *pass|secret|token|key|auth|credential* (trừ tham chiếu os.environ/${VAR} — là mã, không phải bí mật), UUID, mọi token ≥16 ký tự trông ngẫu nhiên (entropy ≥3,7; loại trừ tên file có dấu thời gian, đường dẫn), và dạng văn xuôi password "…". Đo lại: 3 cặp sống sót → sau vá dạng văn xuôi còn 1 và đó là dương tính giả (tên file trong một nhãn thư mục). ⚠️ Lần đo cuối cùng sau vá cuối chưa chạy lại được vì hộp cát của Claude Code chặn lệnh đọc file bí mật; chủ chạy cd /root/mcp-nolimit-stage && python3 leakscan2.py backups,lark-backups,secrets,data,lark-snapshots để có con số cuối (kỳ vọng 0 cặp thật).
  • 🔴 Đề nghị (chưa làm, ngoài phạm vi): xoay 3 bí mật trên và gỡ chúng khỏi mã/nginx conf (nginx có thể include từ secrets/ như các route MCP đã làm). Đây là vấn đề vệ sinh có sẵn, gốc code chỉ làm nó lộ ra; gốc gh là repo PUBLIC nên đọc-từ-code-rồi-ghi-sang-gh là đường rò rỉ cần nhớ khi cho AI làm việc tự do.
  • Đo qua MCP thật: claude-mcp/.env → từ chối (denylist), backups/ → "nằm NGOÀI gốc", docker/nginx/conf.d/default.conf đọc được với giá trị khoá hiện [REDACTED], ghi/sửa/chuyển ở gốc coderoot_read_only.

10.4 Khảo sát directus_* (chỉ khảo sát, CHỜ DUYỆT)

  • Hiện trạng: container incomex-directus (tạo lại 2026-07-23) chỉ ở mạng docker_incomex; claude_mcp_net còn postgres, incomex-nginx, incomex-claude-kb, incomex-cowork-mcp, incomex-claude-mcp. Container claude-mcp không phân giải được tên directus ⇒ mọi directus_* trả upstream_error. Lần thành công cuối 2026-05-18 (2 lần trong toàn bộ audit).
  • Vì sao rời mạng: compose chính docker/docker-compose.yml khai claude_mcp_net là mạng ngoài nhưng không service nào dùng nó (directus.networks = [incomex]); việc gắn Directus vào mạng hồi tháng 5 làm bằng tay (scripts/ensure-claude-mcp-network.shdocker network connect) và mất khi container được tạo lại 23/07. Không có tài liệu/commit nào ghi quyết định tách ⇒ không phải quyết định an toàn cố ý, là trôi cấu hình (bước tay chưa được ghi vào compose). Thiết kế gốc (docs/design-mcp-remote-server.md §3H) vẫn coi Directus thuộc mạng này.
  • Đề xuất nối lại (2 mức, đều chưa làm): (1) tức thời: chạy bash /opt/incomex/claude-mcp/scripts/ensure-claude-mcp-network.sh (idempotent, chỉ docker network connect, không tạo lại container) — quay lui: docker network disconnect claude_mcp_net incomex-directus; (2) bền: thêm claude_mcp_net vào services.directus.networks trong compose chính — đây là file của stack GPT/Codex đang sửa, nên chờ Codex xong. Lưu ý mạng internal: true nên Directus nối vào vẫn không có thêm đường ra internet.
  • Cần chủ cân nhắc trước khi duyệt: directus_create/update/delete là công cụ ghi (Directus API, allowlist 7 collection) — nối mạng lại là mở lại cả phần ghi, khác với "DB chỉ đọc" của query_pg.

10.5 Nghiệm thu (gọi MCP thật qua nginx → container, trên bản sao)

Ca Kết quả
đọc trọn file 314KB 1 lần (max_bytes=400000) ✅ 256/256 dòng, 316.660 byte, 1,9s, HẾT FILE
tìm trong file 2,2MB nén (trước bị bỏ qua) ✅ 75 khớp trên 1 dòng, 0,1s
1 fs_edit 50 chỗ sửa ✅ commit 95f3e31, 0,7s
fs_move gốc ui + fs_log follow ✅ commit 40ae08b, lịch sử 3 commit đi theo tên mới; đích đã tồn tại → từ chối
fs_move gốc gh 5a6792c push main, follow thấy commit trước đổi tên
ghi 19,8MB rồi đọc đuôi, sửa 1 chỗ, tìm trong đó ✅ 3,0s / 0,7s / 7,3s / 1,9s; 21,8MB → từ chối đúng trần 20MB; ghi đè sai version → từ chối
chụp full_page trang dài ✅ 1280×4988 (815KB) và 1280×7513 (1,15MB); selector=body @x2 → 2560×8000 + "chụp tiếp offset_y=4000"; allow_oversize → 2560×15046; 3840 rộng OK; host lạ / selector không có → từ chối; 15360×8000 mặc định → từ chối kèm hướng dẫn
query_pg 5000 dòng ✅ 5000 hàng, 59KB, 0,2s, next: offset=5000; offset 6998 → 2 hàng cuối; timeout_s=30 OK; UPDATE → DENIED
pg_schema ✅ chạy (1 bảng và cả DB 1041 bảng: trả 68 bảng trong 50KB + tên 973 bảng còn lại)
gốc code ✅ đọc claude-mcp/app/server.py; .env từ chối; backups/ ngoài gốc; nginx conf che khoá
vps_status logs n=1000 since=2h contains=INFO (docker) + journald ✅; service="…; cat /etc/shadow"since="1h; id" → từ chối
thử phá script root 35/35 bị từ chối/đúng (helper/host-install/thu-pha-priv.sh: 8 docker-logs cũ + 9 mới cho since/contains/n + 6 ui-commit cũ + 11 ui-move + sudo lệnh khác), mỗi ca 1 dòng journald (32 REFUSED)

Bản sao/chứng tích để lại (không xoá vì không có công cụ xoá, chủ quyết): ui nolimit-test-20260918/ (moved/edit50-renamed.html, big20mb.txt 20MB — 2 bản trong sổ git), gh acceptance/moved/gpt-concurrency-note.txt, ảnh test /root/mcp-nolimit-stage/out/*.png, thư mục stage /root/mcp-nolimit-stage/.

10.6 Nợ mới / lưu ý

  • N8 ⚠️ ứng dụng chat: kết quả 1MB chữ hay ảnh >8000px có thể bị client cắt/từ chối (Claude Code tự cắt kết quả MCP >25k token) — trần đã là của hạ tầng phía nhận, không còn của đầu nối.
  • N9 regex fs_search do người gọi viết có thể "nổ" trên 1 dòng dài (Python re không ngắt được giữa chừng); ngân sách thời gian kiểm giữa các dòng, cpu container 0.5 chặn phần còn lại.
  • N10 drift-check sẽ thêm khác biệt mcp-compose (như N5).
  • N1 ĐÓNG, N4 ĐÓNG (full_page 30000px), N2 còn (chờ duyệt §10.4). Bí mật gõ cứng §10.3 chưa xoay.

11. ĐỢT 1e — 2026-09-18: hết lỗi "file nhị phân" giả · trần tài nguyên · đo token · dọn file thử · khảo sát bí mật

Người làm: Claude Code (Fable 5.1). Chỉ đụng claude-mcp (trợ lý host không đổi). Git VPS 2b77591. Image claude-mcp-local:phase0.8-1e (Dockerfile.phase08 = app/ chồng phase0.5a, không cài gì). Đĩa 86% trước và sau (build ~1MB; clone 6 repo để quét: 52MB trong /root/mcp-nolimit-stage/ghscan/). Mọi "ĐẠT" đo bằng lời gọi MCP thật qua nginx.

11.1 Bảng việc

Việc Kết quả Bằng chứng
0 — Lỗi "file nhị phân" giả ✅ ĐẠT _CONTEXT.md (9.095B, có ô cắt đúng byte 8192): trước = fs_read/read_file từ chối, fs_search "Không khớp" im lặng (scanned=0); sau = đọc trọn 144 dòng, tìm thấy 4 khớp. Quét 4 gốc: logic cũ kết luận nhầm 911 file (ui 20 · docs 21 · code 870 — toàn .md/.csv/.js/.html tiếng Việt), logic mới 0 (mọi file đó decode trọn UTF-8). 7 ca kiểm thử tests/test_is_binary.py (ký tự 2/3/4 byte vắt qua byte 8192; nhị phân thật vẫn bị chặn). fs_search nay đếm file bỏ qua: skipped_binary=, skipped_too_large=
1 — Trần tài nguyên ⚠️ NỬA cpus 0.5→2, mem 512m→1g (đo docker inspect), time_budget_s mặc định 7→20, chú thích "512 MB" đã sửa. Nhưng gốc code KHÔNG quét hết được — xem 11.2 và CHỦ CẦN QUYẾT #1
2 — Nối lại Directus ✅ ĐẠT (có điều kiện) Đã nối incomex-directus vào claude_mcp_net (alias directus, IP 172.19.0.7). directus_read(tac_publication) trả total=4, tac_change_set total=0. 🔑 Token của đầu nối là người dùng Directus tac-agent (policy tac-agent-policy) — chỉ đọc/ghi được các collection tac_*; information_unit, species… trả 403 (quyền, không phải mạng). Không chạy nguyên script ensure-claude-mcp-network.sh vì nó nối luôn cả incomex-agent-data (đang không ở mạng này) — vi phạm luật "không đụng agent-data"; chỉ chạy đúng 1 lệnh docker network connect --alias directus mà script dùng cho Directus. Compose chính chưa sửa (ghi nợ N11)
3 — Đo token ✅ ĐẠT vps_status(muc='usage'): đọc audit.jsonl trong container, không thêm lệnh host. Bảng theo công cụ: lần gọi · bytes · ms · cut · deny · err cho 24h và 7 ngày. Số 24h lúc đo: 153 lần gọi, 9,1MB trả về (ui_screenshot 7,5MB = 82%), 241s chạy
4 — Gom file thử ✅ ĐẠT fs_move (lịch sử đi theo, fs_log follow=true thấy 4 commit của big20mb.txt). ui: _luu-tru-chung-tich-claude/ {nolimit-test-20260918/big20mb.txt, …/moved/edit50-renamed.html, mow-unified-canvas-v2-mcp-test.html, INDEX.md} — commit 1ec1f75 · a965b76 · ac708e2 · 90a25bd. gh: _luu-tru-chung-tich-claude/ {acceptance/gsm-reload-proof.md, INDEX.md} — c6d5053 · a22d267, push main. README.md gh thêm 1 đoạn "file thử của GPT — agent đừng đọc cả file" (0142f57). File GPT không di chuyển. Còn lại 3 thư mục rỗng nolimit-test-20260918/{,moved,sub} (không có công cụ xoá)
5 — Bộ che strict ✅ ĐẠT Thêm miễn che cho tên "giống chữ" (Dockerfile.phase07, phase0.7-nolimit, pwd-2026-abc). Đo lại leakscan2 trước và sau đều: 30 giá trị bí mật · 80.360 file · 53 file chứa bí mật · 1 cặp sống sót = dương tính giả đã biết (tên file trong nhãn thư mục nginx) ⇒ 0 bí mật thật lọt thêm. Che nhầm trên file thật: compose 8→3, 00-NHAN-THU-MUC.md 3→0; khoá trong nginx conf vẫn che
6 — Khảo sát bí mật ✅ XONG, CHỜ DUYỆT xem 11.3 — KHÔNG KHẨN: 6 repo công khai 0 commit nào chứa
7 — Luật hoá ✅ ĐẠT Ghi vào 00-NHAN-THU-MUC.md (mục "LUAT VE TRAN / HAN CHE") + docstring đầu app/limits.py + chú thích compose

CHỦ CẦN QUYẾT

  1. Gốc code quét hết hay không? Gốc có 91.334 file / 5,4GB nhìn từ container: deploys/ 60.620 file (bản dựng Nuxt cũ, 2,9GB) + context-pack.tmp/ 9.597 file (snapshot tự sinh, 1,5GB) + context-pack-staging/ 590 + venv-xlsx/ 3.016. Quét toàn bộ mất 110–137s — vượt cả trần 60s. Loại 4 thư mục đó khỏi gốc ⇒ 13.358 file, quét hết 5–8s (đo thật). Đây là hạn chế mới nên tôi không tự làm. Đề xuất: đồng ý loại 4 thư mục (là bản dựng/snapshot/venv, không phải nguồn) — sửa 1 dòng exclude trong compose; hoặc phương án B: nâng FS_SEARCH_TIME_CAP_S 60→240 (nginx 300s) và người gọi truyền time_budget_s=150.
  2. Quyền Directus cho đầu nối: token hiện là tac-agent (chỉ tac_*). Danh sách ghi của directus_create/update (information_unit, species, knowledge_node…) hầu hết ngoài quyền token ⇒ hiện tại ghi cũng 403. Đề xuất: giữ nguyên (an toàn hơn), chỉ mở thêm quyền đọc nếu Claude Chat thật sự cần đọc Directus thay vì query_pg.
  3. Xoay 3 bí mật — xem 11.3, chờ duyệt thứ tự.
  4. mow-danh-tu-v2-copy.html (gh/acceptance, chủ tạo từ Mac cho nghiệm thu 17/09) tôi không gom vì không phải Claude tạo — chủ muốn gom thì 1 lệnh fs_move.

11.2 Số đo trước / sau

Phép đo Trước (phase0.7, cpus 0.5) Sau (phase0.8-1e, cpus 2)
fs_read _CONTEXT.md từ chối "nhị phân" 144/144 dòng, HẾT FILE
fs_search công cụ trong _CONTEXT.md 0 khớp, scanned=0, im lặng 4 khớp, scanned=1
fs_search 1 chuỗi toàn gốc code, ngân sách mặc định 7,0s · 2.515 file · dừng sớm 20,0s · 7.248 file · dừng sớm (tiền kiểm cả file bằng regex C trước khi lặp dòng: +25% file/giây)
fs_search toàn gốc code, time_budget_s=300 110–137s · 80.227 file quét hết, 11.108 nhị phân, 2 file >64MB
… nếu loại deploys/context-pack.tmp/context-pack-staging/venv-xlsx 5–8s · 13.358 file quét hết
Đọc trọn 3 tài liệu tiếng Việt thật (max_bytes=1000000) mow-unified-canvas-v2.html 159KB/2.428 dòng 0,1s · BAN-DO-HE-THONG-20260729.md 129KB/1.998 dòng 0,1s · AGENTS.md 18KB/364 dòng — không lỗi, không mất dấu; recovery/directus-backup-restore-runbook.md (trước bị coi nhị phân) 291 dòng OK
Che nhầm (số chỗ [REDACTED]) compose 8 · nhãn thư mục 3 compose 3 · nhãn 0 · nginx key vẫn 2
Thử phá script root 35/35 35/35 (chạy 2 lần)

11.3 Khảo sát 3 bí mật gõ cứng (CHỈ KHẢO SÁT — chưa xoay)

Kết luận đầu tiên: KHÔNG KHẨN. Clone bare 6 repo công khai của Huyen1974 (agent-data 516 commit/62 nhánh · agent-data-production 14 · agent_data 1 · mpc_back_end_for_agent 0 · LarkBase_OAuth_Policy 1 · incomex-workspace 31) và git log --all -S<giá trị> cho cả 3 giá trị: 0 commit từng chứa, HEAD 0 file. Hai repo chứa trong lịch sử là agent-data-test (API key: 7 commit, 4 file đang tracked; mật khẩu PG: 3 commit, 2 file) và web-test (API key: 10 commit, 5 file; MySQL root: 1 commit, 1 file) — cả hai PRIVATE (xác nhận bằng gh api đã đăng nhập). Giá trị không bao giờ được in; kịch bản /root/mcp-nolimit-stage/{secret-survey,gh-history-scan,local-repo-scan}.py chỉ in số đếm/đường dẫn.

Bí mật Ai đang dùng (xoay sót thì chết) Nằm ở đâu (file sống, ngoài .bak) Lưu ý
AGENT_DATA_API_KEY (UUID) container incomex-agent-data (env API_KEY — bên kiểm), incomex-nuxt (NUXT_AGENT_DATA_API_KEY), incomex-claude-kb (AGENT_DATA_API_KEY), incomex-directus (AGENT_DATA_API_KEY, flow gọi agent-data); nginx docker/nginx/conf.d/default.conf (gõ cứng trong proxy_set_header, cả secrets/gpt-mcp-route.conf); DOT dot/bin/dot-knowledge-ingest-batch, dot-vector-audit; scripts/reconcile-*.py, test-agent-connections.sh; nuxt-repo/automation/config.js 186 file trên đĩa (56 dưới docker/, 33 /root/agent-runs worktree, 23 evidence/, 22 /root/incomex-config-changes…); nguồn: docker/.env, secrets/.env.production, /etc/incomex/agent-data.env, claude-kb/.env Xoay = đổi đồng thời ≥4 container + nginx + 4 file DOT/script; lỡ 1 chỗ là Nuxt/KB/Directus-flow gọi agent-data 401
INCOMEX_PG_PASSWORD (30 ký tự; ≠ POSTGRES_PASSWORD ≠ PG_PASSWORD ≠ DB_PASSWORD) container incomex-agent-data (env PG_PASSWORD) — duy nhất docker/.env, /etc/incomex/agent-data.env, agent-data-repo/scripts/import_firestore_to_pg.py, agent-data-repo/.github/workflows/s109-pg-setup.yml, 1 file evidence luật Điều 33 Xoay dễ nhất: ALTER ROLE + 2 file env + restart agent-data; ~30s mất kết nối agent-data
MYSQL_ROOT_PASSWORD không container nào — MySQL đã nghỉ hưu 13/03/2026 (S115, compose ghi rõ) docker/.env, 2 báo cáo W205_fix_db_auth_safe_password.md (nuxt-repo + web-rp-current) Không còn dịch vụ ⇒ "xoay" = xoá dòng khỏi .env + che trong 2 báo cáo; rủi ro 0

⚠️ Đang có việc song song: /root/incomex-config-changes/hardening-20260918-env-docker-stack/ (hôm nay, không phải tôi) đang sờ vào docker/.env — xoay phải phối hợp với bên đó. incomex-agent-data bị tạo lại 2 lần trong đợt (03:19Z và 03:47Z, die → destroy → start; không do đợt này — compose overlay claude-mcp chỉ có 1 service).

Đề xuất thứ tự xoay (chờ duyệt): (1) MYSQL_ROOT_PASSWORD — xoá, 0 phút chết; (2) INCOMEX_PG_PASSWORD — ALTER ROLE trong PG → sửa docker/.env + /etc/incomex/agent-data.envdocker restart incomex-agent-data → xoá giá trị khỏi 2 file trong agent-data-repo (đọc từ env); chết ~30s; lùi = ALTER ROLE về giá trị cũ; (3) AGENT_DATA_API_KEY — sinh UUID mới → sửa .env của 4 container + nginx default.conf/gpt-mcp-route.conf (nên include từ secrets/) + 4 file DOT/script → restart agent-data, nuxt, claude-kb, directus, nginx -s reload; chết 1–2 phút; lùi = trả .env + conf từ bản sao .bak-<ts> rồi restart. Sau xoay: chạy lại secret-survey.py phải ra 0 file sống chứa giá trị cũ; các bản .bak/worktree /root/agent-runs vẫn chứa giá trị (vô hại sau xoay, nhưng nên dọn — chủ quyết).

11.4 Cách quay lui đợt 1e

  1. Container: docker tag sẵn claude-mcp-local:rollback-pre-1e-20260918 (= phase0.7-nolimit) + docker-compose.claude-mcp.yml.bak-1e-20260918T030949Z → chép đè compose → docker compose -f docker-compose.claude-mcp.yml up -d claude-mcp (về cpus 0.5/512m, mã cũ — lỗi "nhị phân" giả quay lại).
  2. Directus: docker network disconnect claude_mcp_net incomex-directus.
  3. File đã gom: fs_move ngược lại từng file (lịch sử vẫn theo). Trợ lý host không đổi, không cần lùi.

11.5 Nợ mới

  • N11 compose chính docker/docker-compose.yml chưa có claude_mcp_net cho Directus — Directus tạo lại là rơi mạng lần nữa (file của Codex, chờ Codex xong).
  • N12 3 thư mục rỗng trong ui nolimit-test-20260918/ — không có công cụ xoá.
  • N13 ensure-claude-mcp-network.sh nối cả agent-data — muốn dùng script phải bỏ mục đó hoặc chủ duyệt riêng.
  • N14 patch_document của đầu nối agent-data qua Claude Code gửi old_str rỗng (lỗi cầu nối MCP, không phải KB) — §11 này ghi bằng REST PATCH /documents/… trực tiếp.
  • N2 ĐÓNG (mạng), nhưng quyền Directus của token chỉ tac_* (CHỦ CẦN QUYẾT #2).

12. ĐỢT KẾT NỐI CHUNG GPT + CLAUDE — 2026-09-18 (Claude Code) — VÒNG 1 + VÒNG 2

Đọc nhanh: vòng 1 ở 12.0–12.9 (giữ nguyên làm hồ sơ). VÒNG 2 — đóng nợ môi trường — ở 12.10–12.15, kèm bảng trước/sau, OWNER CẦN QUYẾT mới và đường lùi từng lớp.

Prompt: "Hoàn thiện kết nối GPT + Claude vào GitHub/VPS" (rev 2, Fible). Mục này được ghi lại sau MỖI nhóm việc để một phiên mới nối tiếp được. Trạng thái: 🟢 PASS · 🟡 cảnh báo · 🔴 FAIL/BLOCKED · ⚪ OPEN.

12.0 Bảng kết quả (cập nhật liên tục)

Năng lực GPT backend GPT Chat/Work/Codex Claude backend Claude Chat/Cowork Shared test OPEN + lý do + bên xử lý
Giai đoạn 0 (định vị, khoá phối hợp) 🟢 🟢
C5 KB continuation (>30KB tới EOF, budget, quyền result) 🟢 không đổi 🟢 19/19 qua nginx (claude-kb-local:c5-20260918) Claude Chat/Cowork chưa chấm — Fible: đọc báo cáo này bằng get_document_for_rewrite + kb_result_read
C6 restart/readiness 🟢 không đổi 🟢 phiên cũ → 404 đúng chuẩn; trợ lý chết → helper_unavailable 0,09s; readiness 🟡 Claude Code (cầu nối Anthropic): sau restart bị 502 ~10 phút rồi tự nối lại (nginx: 404 → initialize 200 → list 200); Claude Chat chưa đo 502 nằm ở proxy Anthropic, không phải VPS — chỉ ghi nhận
S1 khoá ui chung 🟢 không đổi mã 🟢 cùng inode 1313457 🟢 hai chiều (đo lại có đồng bộ: Claude giữ → GPT WORKSPACE_BUSY; GPT giữ → host & Claude bị chặn)
S2 phiên bản + HEAD 🟢 sẵn có 🟢 expected_head + expected_version 🟢 (Claude Code) ⚪ ca A/B GitHub hai chiều cần GPT ghi thật Fible + GPT
S3 idempotency + mất phản hồi ⚪ chỉ upload/commit idempotent 🟢 operation_id, OUTCOME_UNKNOWN → 1 commit (đo thật) GPT operation_id cho edit/transaction — Codex
C1 fs_stat / fs_copy / fs_transaction 🟢 sẵn có 🟢 33/33 (vòng 2 đã sửa bộ đếm: ca "FAIL" là BÁO ĐỎ GIẢ do git trích dẫn tên có dấu, không phải sản phẩm — xem 12.10) 🟢 fs_stat/fs_copy/fs_transaction qua cầu nối Claude Code; Claude Chat ⚪
C2 search continuation 🟢 sẵn có 🟢 cursor tới EOF; cả gốc code complete; include/exclude; generated; regex lồng từ chối
C3 ui_inspect / ui_act 🟢 sẵn có (ui_inspect) 🟢 19/19 trên trang fixture (chữ có dấu, modal, gửi form, che token/password, 404, chặn chuyển hướng) 🟢 ui_inspect qua Claude Code profile đăng nhập: OPEN (chưa có account test qua GSM)
C4 chuyển file 🟢 sẵn có ⚪ Work attachment ⚪ khảo sát: fs_write 20MB text; nhị phân chỉ fs_copy trong VPS ⚪ claude.ai không chuyển attachment tới MCP từ xa chiều download/export: OPEN; upload nhị phân: OPEN (đề xuất fs_upload_* nếu chủ cần ảnh vào ui)
Vá GPT 🟢 lock: không cần vá; docs: đã thêm mục hợp đồng (e8e7715, không restart) ⚪ Codex cache/reconnect idempotency GPT: OPEN
Tải phối hợp (mục 7) 🟢 🟢 🟢 4 Claude + 3 GPT đồng thời đều OK, tải đỉnh 2,83/6 CPU, claude-mcp 79MiB/1G, agent-data 2,4G/6G, RAM trống không đổi, health OK
README kho chung (Shared Workspace Contract v1.0) 🟢 ghi qua chính fs_edit (commit 8ba6a79)

OWNER CẦN QUYẾT (mỗi câu có sẵn đề xuất — gật/lắc)

  1. ĐÃ LÀM Ở VÒNG 2 (chủ đã duyệt) — xoay khoá nội bộ MCP_UPSTREAM_KEY (nginx → container claude-mcp; không phải path-secret): tôi in nhầm file route nginx ra terminal CLI (path-secret đã che, dòng X-API-Key thì không). Rủi ro thấp (chỉ dùng được sau path-secret, trong mạng nội bộ). Đề xuất: gật → tôi xoay (.env claude-mcp + route nginx trong secrets/ + nginx -s reload + restart claude-mcp; ~1 phút) — cần duyệt vì đụng nginx.
  2. Chứng tích thử ở gh _luu-tru-chung-tich-claude/p09-*/ (9 commit test trên main) và ui _luu-tru-chung-tich-claude/{fixture-p09,p09-*} — không có công cụ xoá. Đề xuất: giữ.
  3. GPT idempotency (operation_id cho workspace_edit/workspace_transaction): Đề xuất: để Codex làm khi có quota; tôi KHÔNG đụng agent-data đợt này.
  4. Upload nhị phân vào ui từ Claude Chat (ảnh cho trang web): hiện chưa có đường; Đề xuất: chưa làm — chỉ làm nếu chủ thật sự cần (sẽ là fs_upload_begin/append/commit base64, model phải chép từng mảnh = tốn token, không phải native).
  5. Tài khoản test cho trang cần đăng nhập (ui_inspect/ui_act với profile): chưa có nguồn bí mật hợp lệ. Đề xuất: để OPEN cho tới khi có nhu cầu thật.

12.1 Giai đoạn 0 — đo live 05:40–06:00 UTC

  • Không có việc dở của ai đang chạy: không tiến trình build/rsync/deploy; Codex hết quota; commit cuối của Codex a483d09 (03:56 UTC, "Deploy bounded MCP workers…"); hardening-20260918-*/root/incomex-config-changes/ sửa lúc 03:25–03:27 UTC (chỉ reviewed.conf của config-guard, không phải deploy đang chạy). incomex-mcp-helper.service bị ai đó restart lúc 05:01 UTC (journald: Stopping→Started; file trợ lý không đổi, hash y nguyên bản 1e) — không phải tôi; ghi nhận, không kết luận.
  • Repo VPS /opt/incomex: nhánh feat/s177-sprint1-round-a, 170 mục dirty của bên khác (data/phai-cu/*, docs/mcp-writes/* do sổ ui, docs/mcp-writes-notebook/*) — không đụng; tôi chỉ git add đường dẫn cụ thể dưới claude-kb/, claude-mcp/. Không có remote. agent-data-repo: nhánh codex/mcp-hardening-20260918 = bf64bbb, sạch; image chạy agent-data-hardening:20260918-final (tạo 03:47 UTC, healthy, 37 tool). Claude: claude-mcp-local:phase0.8-1e, claude-kb-local:v1 (→ c5). 12 container, 0 lỗi. Đĩa 14G trống (86%), RAM 12G, tải ~0.4–0.7.
  • S1 đã có câu trả lời (đo bằng docker inspect mount + realpath):
resource_id GPT root Claude root host realpath remote/branch chế độ commit/push khoá
ui ui = /workspace/ui rw, execution=true ui rw, vcs=git-local CÙNG THƯ MỤC /opt/incomex/docs/mcp-writes — (GPT: filesystem, không git) / Claude: sổ /var/lib/incomex/ui-preview-notebook.git + snapshot 5 phút GPT: atomic replace, không commit · Claude: commit trước+sau mỗi sửa GPT: flock EX/SH trên /opt/incomex/data/workspace-tools/<sha256("ui")>.lock · Claude: flock trên /tmp/fsroots-ui.lock trong containerCHƯA CHUNG
gh / workspace workspace = /workspace/state/github-workspace (git, main, push) gh (git-push, main) HAI CLONE RIÊNG: /opt/incomex/data/workspace-tools/github-workspace (GPT, 7fcff90) và /opt/incomex/mcp-roots/gh (Claude, 0142f57 — chậm hơn origin 1 commit, tự pull trước khi ghi) cùng git@github.com:Huyen1974/incomex-workspace.git main cả hai: pull/fetch → commit → push không force; conflict qua HEAD remote không cần khoá chung (mỗi clone khoá riêng); bảo vệ = push non-force
docs ro ro cùng /opt/incomex/docs không cần
agent-data rw + exec (không có; code chỉ đọc nhìn thấy nó qua /mnt/code/docker/agent-data-repo) /opt/incomex/docker/agent-data-repo git nhánh codex/…, không push GPT only GPT
code ro /opt/incomex không cần

Hệ quả: khoá chung CHỈ cần cho ui. Cách rẻ nhất, không đụng agent-data: mount đúng file khoá của GPT vào container Claude và cho _RootLock('ui') dùng nó (cùng inode ⇒ flock chung); GPT ghi giữ EX, đọc giữ SH; Claude sẽ giữ EX khi ghi và SH khi đọc ui. Snapshot 5 phút của Claude gom cả thay đổi GPT ghi thẳng — hợp đồng: coi đó là lịch sử chung, không rollback.

  • Nguồn tham chiếu đã đọc: claude-mcp/00-NHAN-THU-MUC.md, app/*.py, helper/mcp_host_helper.py, claude-kb/app/server.py, agent-data-repo/docs/WORKSPACE_TOOLS.md, agent_data/workspace_tools.py (lock, page_result), workspace_transfer.py (result_read), agent_data/server.py (dispatcher). .claude/skills/incomex-rules.md không tồn tại trên VPS (/opt/incomex/.claude/skills/ không có) — ghi nhận. Audit GPT quy trình/ket-noi-gpt/audit.md chỉ có trên Mac (chưa đọc — không cần cho phần đã làm).

12.2 C5 — Claude KB hết ngõ cụt — 🟢 LIVE 05:58 UTC

  • Nguyên nhân đo được: agent-data gói MỌI kết quả >6000 ký tự (response_page_chars) thành {result_id, content=6000 ký tự đầu, continuation_tool:"workspace_result_read"}; proxy Claude KB chặn workspace_result_read (allowlist chỉ-đọc) ⇒ Claude Chat thấy đúng 6000 ký tự và hết đường. Ví dụ: get_document_chunk(max_chars=20000) của chính báo cáo này → 6000.
  • Sửa (chỉ claude-kb/app/server.py, agent-data KHÔNG đổi): (1) proxy nhận gói → tự gọi workspace_result_read nội bộ, lắp lại trọn kết quả, kiểm SHA256 agent-data khai; (2) trả cho mô hình theo cửa sổ page_chars (tham số thêm vào schema mọi tool KB; mặc định env KB_PAGE_CHARS=40000, trần KB_PAGE_CHARS_CAP=1M) — vừa thì trả nguyên JSON gốc, dài thì {result_id, content, offset, total_chars, sha256, next_cursor, continuation_tool:"kb_result_read"}; (3) tool mới kb_result_read(result_id, offset, max_chars) chỉ đọc result trong sổ đăng ký của proxy (result do tool KB chỉ-đọc tạo qua chính proxy) — result của tool GPT (exec stdout, upload…) hay UUID đoán → RESULT_NOT_OWNED; workspace_result_read vẫn bị chặn/không quảng cáo; (4) annotations readOnlyHint cho 7 tool; (5) /readyz + lỗi UPSTREAM_UNAVAILABLE kèm retry_after_seconds khi agent-data chưa sẵn sàng. Sổ đăng ký trong RAM (KB_RESULT_CACHE_BYTES 48MB, TTL KB_RESULT_TTL_S 7 ngày = TTL của agent-data; hết thân thì tự tải lại và kiểm lại SHA). Phạm vi quyền = principal đầu nối (mọi phiên Claude Chat dùng chung path secret này đều đọc được result của nhau — cùng chủ; không tách theo Mcp-Session-Id để reconnect giữa chừng không mất con trỏ).
  • Nghiệm thu 19/19 qua nginx → route thật (/root/mcp-c5-stage/kb-c5-test-nginx.py, chỉ in số): tools/list 7 tool; chunk 20000 về trọn (20.647B, 0,1s); get_document_for_rewrite báo cáo này 33.489 ký tự với page_chars=10000 → 2 trang tới EOF, SHA khớp, JSON parse, body 32.765 ký tự; max_chars=1234 đúng; offset quá EOF/result_id sai/UUID lạ/result do GPT tạo (page_result gọi trong container agent-data) → từ chối rõ; gọi thẳng workspace_result_read → "Unknown tool"; mặc định 40k: 1 trang; get_document/search_knowledge/list_documents/batch_read(full=true, 2 tài liệu, 33.616 ký tự) đều tới EOF.
  • Triển khai: image claude-kb-local:c5-20260918 (Dockerfile.c5 = app/ chồng v1, không cài gì; build ~0 byte thêm), compose thêm 6 env KB_* có lý do; container healthy, hash /app/app/server.py trong container = nguồn (39828457…). Lùi: claude-kb-local:rollback-pre-c5-20260918 (= v1) + docker-compose.claude-kb.yml.bak-c5-20260918T055718Z + app/server.py.bak-c5-20260918T055718Zcompose up -d. Git VPS 818c5ed.
  • Còn OPEN: Claude Chat/Cowork chưa chấm (chỉ CLI qua endpoint = PASS backend) — Fible đọc thử báo cáo này bằng get_document_for_rewrite + kb_result_read tới EOF. Client cũ có thể cần "làm mới công cụ" để thấy kb_result_read + page_chars.

12.3 ĐỢT 0.9 — S1–S3 + C1 + C2 + C6 — 🟢 LIVE 07:15 UTC (git VPS 195b7c8)

  • Image claude-mcp-local:phase0.9 (Dockerfile.phase09 = app/ chồng phase0.5a, không cài gì); compose thêm mount file khoá của GPT /opt/incomex/data/workspace-tools/<sha256("ui")>.lock → /shared-locks/ui.lock:ro (flock không cần quyền ghi) + env FS_OPS_JOURNAL, FS_SEARCH_GENERATED, FS_TRANSACTION_MAX_OPS, HELPER_*, FS_WAIT_* (lý do cạnh từng biến). Trợ lý host: mcp_host_helper.py thêm gh_apply_commit_push (giao dịch nhiều file = 1 commit, hoàn tác cả file mới tạo), expected_head, dấu nhịp .helper-alive; script root + sudoers KHÔNG đổi (35/35 thử phá). Lùi: claude-mcp-local:rollback-pre-09-20260918 (= phase0.8-1e) + docker-compose.claude-mcp.yml.bak-09-20260918T070751Zcompose up -d; trợ lý: /root/mcp-helper-rollback-09-20260918/mcp_host_helper.py + systemctl restart; tar toàn bộ trước sửa /root/mcp-c5-stage/claude-mcp-pre09-20260918T070751Z.tgz. Hash /app/app/fsroots.py trong container = nguồn (6d98187f…).
  • 23 tool (18 cũ + fs_stat, fs_copy, fs_transaction, ui_inspect, ui_act — đếm lại live 18/09 bằng tools/list có bắt tay MCP đầy đủ; con số "21" ở các bản trước là SAI vì bỏ sót hai tool ui_*), mọi tool có annotations (readOnlyHint/destructiveHint/idempotentHint); lỗi trả [DENIED:<mã>] … để client rẽ nhánh (busy, version_mismatch, head_mismatch, outcome_unknown, helper_unavailable, secret_in_content, regex_unsupported…).
  • Hợp đồng đã cài (tóm tắt để ghi README kho chung): (1) ui = một thư mục vật lý, một khoá flock chung — ghi EX, đọc SH, chờ ngắn rồi BUSY+retry; snapshot 5 phút của sổ ui gom cả thay đổi GPT = lịch sử chung, không rollback; (2) gh/workspace = hai clone, cùng main: pull/fetch → kiểm expected_head/expected_version → commit đúng file → push non-force; remote đổi → conflict có mã, đọc lại; (3) mọi ghi có operation_id: cùng id+cùng payload = replay, khác payload = từ chối, mất phản hồi = OUTCOME_UNKNOWN → gọi lại cùng id để giải quyết; (4) nội dung vào gốc công khai (gh PUBLIC, ui web) qua mọi đường ghi đều qua bộ quét bí mật (JWT/bearer/api-key/PEM/DSN/password/path-secret), gốc code không bao giờ là nguồn copy; (5) thư mục sinh tự động bị bỏ khi tìm, include_generated=true để quét hết.
  • Nghiệm thu (kịch bản /root/mcp-c5-stage/p09-test.py, chạy trong container trên claude_mcp_net qua nginx, chỉ in số/đầu dòng): 32/33 PASS — ca "FAIL" duy nhất là kịch bản đếm +++ b/ trong khi git trích dẫn đường dẫn có dấu ("…/ghi-ch\303\272.md"); git show --stat 359c0fd xác nhận 3 files changed, 1 commit. Unit 7/7 (tests/test_fsroots_phase09.py, chạy trên host). Khoá chung đo hai chiều (mục S1). Mất phản hồi: container thử FS_WAIT_PUSH_S=0.3 → lần 1–4 trợ lý từ chối expired (chưa nhận trước hạn ⇒ KHÔNG làm), lần 5 outcome_unknown (trợ lý đang push), lần 6 resolved commit=358a3dd, lần 7 REPLAY; git log file = 1 commit. Trợ lý dừng 35s → readiness DOWN, fs_list gh trả sau 0,09s kèm helper_unavailable, fs_read docs bình thường; bật lại → UP.
  • Số đo: fs_stat gh (kèm last_commit) 2,25s (pull throttle + 2 lượt trợ lý); fs_copy ui 0,35s; docs→gh push 3,5s; tx gh 3 file 4,9s; tx ui 3 op 1,2s; search cursor 7 cửa sổ cho 173 khớp; cả gốc code 13.421 file 23,3s (time_budget_s=60; mặc định 20s sẽ trả partial + cursor).
  • Chứng tích để lại: gh _luu-tru-chung-tich-claude/p09-*/ (8 commit trên main, kể cả 2 lần chạy), ui _luu-tru-chung-tich-claude/{fixture-p09/pixel.png, p09-*/}; nhật ký /var/log/mcp-audit/fs-ops.jsonl (+ fs-ops-test.jsonl của container thử).

12.4 C3 — ui_inspect + ui_act — 🟢 LIVE 07:29 UTC (git 05a8613)

  • Trợ lý host thêm hành động browse (CDP qua pipe, hồ sơ trình duyệt tạm mỗi lần, không cookie): mở 1 trang thuộc vps.incomexsaigoncorp.vn → (tuỳ chọn) chạy danh sách bước từ vựng cố định click | hover | type | press | scroll_to | wait (không nhận JavaScript; mỗi bước có trace ok/ms/url_after; selector không khớp → dừng rõ; chuyển hướng ra host khác → dừng url_not_allowed) → thu outline (title, headings, landmarks/form, links, controls — password che ***, ảnh, đếm), text (innerText theo max_text, báo tổng/cắt), styles (computed + box), console (error/warning/exception), network (request hỏng / HTTP ≥400; URL có tham số bí mật bị che, không header/body) → (tuỳ chọn) ảnh PNG selector / viewport / full_page ≤8000px. Container: ui_inspect (readOnlyHint, KHÔNG có tham số actions) và ui_act (destructiveHint — có thể gửi form; chỉ dùng trang thử).
  • Nghiệm thu 19/19 (/root/mcp-c5-stage/c3-test.py) trên fixture ui/_luu-tru-chung-tich-claude/fixture-p09/test-modal.html (ghi bằng fs_write): h1 có dấu đúng; modal ẩn không lọt vào text; console error + 2×404 (json, png) bắt được; click mở modal + ảnh selector 1264×149px; type + press Enter gửi form cùng origin (?name=Huy%E1%BB%81n) và text "Xin chào Huyền"; selector sai → [DENIED:selector_not_found]; click link ngoài → [DENIED:url_not_allowed]; host lạ → từ chối. Cũng gọi được qua cầu nối Claude Code (client thật, không phải Claude Chat). Trần mới: BROWSE_MAX_ACTIONS=30, BROWSE_MAX_TEXT=200000, BROWSE_MAX_ITEMS=500 (env cả 2 phía, lý do ghi cạnh) — là trần kỹ thuật của một lần gọi, có đường đọc tiếp qua nhiều lần gọi/selector.

12.5 C4 — chuyển file (khảo sát, chưa dựng thêm)

  • Claude Chat / Cowork → MCP từ xa: claude.ai KHÔNG truyền attachment/resource handle tới tool của remote MCP (chỉ tham số JSON do mô hình điền) ⇒ không có "native transfer" tương đương openai/fileParams của GPT. Cowork đọc được file cục bộ nhưng vẫn phải chép nội dung vào tham số = model-relayed. Kết luận: native = NOT_APPLICABLE cho Claude; đường có sẵn: fs_write (UTF-8 tới 20MB/lần, đo 19,8MB ở đợt 0.7), fs_copy server-side giữ byte (kể cả nhị phân) trong VPS; chưa có đường đưa file nhị phân từ máy người dùng vào ui/gh qua Claude (OPEN #4).
  • Chiều download/export ra client: MCP có thể trả blob (EmbeddedResource) nhưng claude.ai chỉ hiển thị ảnh; text thì fs_read. OPEN, không dựng kho lưu trữ mới.

12.6 Tải phối hợp (mục 7) — 🟢

Chạy đồng thời: Claude fs_search cả gốc code (3.000 khớp, 3,8s) + ui_act click + ảnh full_page (3,3s) + fs_read ui (0,15s) + fs_search ui (0,17s); GPT (đường mã thật trong container) workspace_search (4,3s), workspace_read ui (0,0s — khoá SH chung không cản), ui_inspect Playwright (8,8s). Đỉnh: tải 2,83/6 CPU; claude-mcp 79MiB/1G, cpu 9%; agent-data 2,38G/6G, cpu 74% (Playwright); claude-kb 49MiB; RAM trống 7,3G trước/sau; đĩa 14G; 12 container healthy. Không BUSY, không lỗi. Không giả lập cạn đĩa.

12.7 Tài liệu

  • README kho chung: mục Shared Workspace Contract — v1.0, 2026-09-18 (7 điều: tài nguyên, khoá, phiên bản, commit/push, retry & mất phản hồi, xuất công khai, chi phí) — commit 8ba6a79 ghi bằng chính fs_edit (expected_version). GPT: docs/WORKSPACE_TOOLS.md thêm mục trỏ + ghi chú khoá ui chung (e8e7715 trên nhánh codex, chỉ tài liệu, không restart). Claude: claude-mcp/00-NHAN-THU-MUC.md (2cb27bc). Audit gốc của GPT (quy trình/ket-noi-gpt/audit.md trên Mac) chưa cập nhật — không truy cập từ VPS; phần thay đổi phía GPT chỉ là tài liệu.

12.8 Cách quay lui cả đợt (thứ tự ngược)

  1. claude-mcp: docker-compose.claude-mcp.yml.bak-09-20260918T070751Z → chép đè → compose up -d (image rollback-pre-09-20260918 = phase0.8-1e; mount khoá chung/env mới thành trơ). 2. Trợ lý: /root/mcp-helper-rollback-09-20260918/mcp_host_helper.pysystemctl restart incomex-mcp-helper (container cũ không dùng menu mới). 3. claude-kb: .bak-c5-20260918T055718Z + image rollback-pre-c5-20260918. 4. README/WORKSPACE_TOOLS/00-NHAN: revert commit tương ứng. Không có gì đợt này chạm agent-data image/compose/.env, nginx, Directus, PG.

12.9 Nợ / OPEN tổng hợp

  • N15 GPT operation_id (Codex). N16 ca cross A/B GitHub (GPT ghi thật) + ca "cùng thư mục ui hai đường ghi thật" từ client thật — Fible/GPT. N17 Claude Chat/Cowork chưa chấm C5/C1/C2/C3 (chỉ Claude Code + endpoint). N18 MCP_UPSTREAM_KEY (quyết định #1). N19 upload nhị phân từ Claude / download ra client (quyết định #4). N20 profile đăng nhập (quyết định #5). N21 ghi chú cho Fible: sau restart container, cầu nối Anthropic có thể trả 502 vài phút — đợi rồi gọi lại, không phải lỗi VPS. N22 client_max_body_size 0 trên route claude-mcp (ai đó đã đổi) ⇒ lý do trần FS_WRITE_MAX_BYTES=20MB ("nginx 50m") không còn đúng — có thể nới khi cần (xin phép chủ theo luật).

12.10 VÒNG 2 — đóng nợ môi trường phối hợp — 2026-09-18 (Claude Code, Opus 5)

Prompt chốt "VÒNG 2: ĐÓNG NỢ MÔI TRƯỜNG PHỐI HỢP GPT + CLAUDE" + 2 hiệu chỉnh của chủ (agent-data: chỉ được sửa docs/WORKSPACE_TOOLS.md; staging upload: đo, không xoá). Mọi dòng 🟢 dưới đây đều gọi thật, không suy từ mã.

Hạng mục Trước Sau Test thật (backend / Claude Chat / GPT client)
Nguồn luật của kho chung HAI nguồn: README v1.0 + AGENTS.md (tự nhận "ưu tiên cập nhật chính file này") MỘT: README v1.1 là hợp đồng; AGENTS.md rút còn cửa vào 17 dòng backend: ghi bằng chính fs_transaction (f21132a), đọc lại bằng fs_search/fs_stat / ⚪ / ⚪ 🟢
Ba đường ghi không được mô tả ở đâu §0: bảng 3 đường + 5 luật riêng cho GitHub native (SHA hiện tại, biết HEAD trước + diff sau, hoà giải không ghi đè, không thay giao dịch nguyên tử, tự kiểm bí mật) backend 🟢 / ⚪ / ⚪ 🟢
Quyền xoá không có điều khoản (GitHub native có delete_file) §7: delete_file chỉ khi chủ duyệt TỪNG LẦN; dọn dẹp = rename vào _luu-tru-chung-tich-*/ backend 🟢 / ⚪ / ⚪ 🟢
Bảo vệ nhánh main GPT báo ruleset rỗng + 403 ⇒ chưa rõ có thật không CÓ THẬT (đo bằng GitHub API quyền admin): allow_force_pushes=false, allow_deletions=false, enforce_admins=true, secret scanning + push protection BẬT. Ruleset rỗng là bình thường — chốt chặn nằm ở branch protection cổ điển backend 🟢 / — / — 🟢
Giới hạn body route claude-mcp client_max_body_size 0 = KHÔNG giới hạn (nợ N22) 50m — đúng trần thiết kế ghi trong app/limits.py, bằng mức server-level của conf.d/default.conf backend 🟢: 1 KB và 29,5 MiB và 42,9 MiB LỌT · 55,3 MiB → HTTP 413 🟢
— cơ sở chọn 50m (đo thật, không ước lượng) yêu cầu ghi 20 MB tiếng Việt sau JSON-escape ensure_ascii = 32,34 MB backend 🟢 — 32m mà tôi đặt lúc đầu là QUÁ THẤP, đã sửa lên 50m trước khi chốt 🟢
MCP_UPSTREAM_KEY khoá cũ (tôi in nhầm ra terminal 18/09 — nợ N18) đã xoay. Rà 2 cách (tên biến + giá trị): 11 file chứa, 3 nơi sống đã cập nhật (claude-mcp/.env, route nginx ×2, .deploy-secrets), 8 nơi còn lại là bản lưu/lịch sử backend 🟢: khoá cũ → 401, khoá mới → 200, qua nginx initialize200 / Claude Code 🟢 nối lại được ngay / — 🟢
Snapshot 5 phút (người ghi THỨ BA) chỉ khoá riêng, chờ 60s, hết hạn = unit FAILED thêm khoá CHUNG vùng ui (chia sẻ, cùng inode 1313457), chờ 15s rồi BỎ LƯỢT, thoát mã 0 backend 🟢 đo thật: bên kia giữ khoá → bỏ lượt sau 15s, HEAD không đổi (303 commit), log SKIP ban-khoa; nhả khoá → commit đủ (304); c7b7aa6 vẫn là tổ tiên của f2cee6f 🟢
config-guard FAILED liên tục, MISMATCH=3 (kb-server-py, kb-compose, mcp-compose) MISMATCH=0 · STATUS=CLEAN, dịch vụ hết FAILED backend 🟢. Nguồn gốc 3 chỗ lệch đã truy: đúng 818c5ed (C5) + 195b7c8 (0.9) của vòng 1, không ai chen vào. Chuông vẫn kêu: giả lệch mode → MISMATCH MODE, hoàn nguyên → CLEAN 🟢
Bộ đếm test 32/33 — 1 ca "FAIL" 33/33 — ca đó là BÁO ĐỎ GIẢ của bộ đếm, sản phẩm luôn đúng backend 🟢: trên commit thật 359c0fd, bộ đếm cũ +++ b/ = 2, bộ đếm mới +++ "?b/ = 3 + nhận ra tên có dấu. Chạy lại thật qua client: 43d3f84 = 3 file / 1 commit 🟢
Idempotency phía GPT OPEN, chưa có bản bàn giao bản bàn giao kỹ thuật đủ cho Codex, viết vào đúng một tài liệu docs/WORKSPACE_TOOLS.md (23214cb) — (chỉ tài liệu; agent-data không restart, vẫn Up 6 hours (healthy)) ⚪ Codex
Luật chi phí 1 dòng chung §9 thêm 2 dòng đo thật: soi bằng CHỮ trước (ảnh 479 KB/lần vs ui_inspect 2 KB/lần, rẻ hơn ~240 lần); ưu tiên thao tác server-side backend 🟢 (không đo lại — dùng số Fible đã đo) 🟢
Khoá chung GPT↔Claude cùng inode 1313457 vẫn cùng inode sau khi tạo lại container để nạp khoá mới backend 🟢 (host 1313457 = trong container 1313457, mount ro còn nguyên) 🟢
Hồi quy Claude MCP fs_stat · fs_copy (tên đích có dấu) · fs_transaction · fs_search cursor · ui_inspect · ui_act · readiness 🟢 qua cầu nối Claude Code (client thật) / Claude Chat ⚪ / — 🟢 backend
KB continuation PASS vòng 1 đọc 52.859 ký tự / 2 trang tới EOF, SHA khớp, has_more=false, next_offset=null 🟢 qua Claude Code / Claude Chat ⚪ 🟢 backend
Dung lượng staging (yêu cầu bổ sung của chủ) chưa đo 62 MB tổng (uploads 56 KB/13 file · results 3,2 MB/58 file · github-workspace 17 MB) — 0 file quá 7 ngày backend 🟢. Đĩa 86% là do backups 5,9G + deploys 2,9G + docker 1,7G + context-pack.tmp 1,5G, không phải cơ chế mới 🟡 xem OPEN #3

[CHỈNH 1 — sửa một dòng sai của bản GPT.] 5 công cụ fs_stat/fs_copy/fs_transaction/ui_inspect/ui_act KHÔNG phải "đã được Claude Code + Claude Chat kiểm". Đúng là: PASS backend + PASS qua cầu nối Claude Code; client Claude Chat/Cowork = ⚪ OPEN. Phiên Claude Chat của Fible mở trước lúc container restart nên giữ bộ schema cũ 18 tool — đây là bộ nhớ đệm schema phía client, không phải lỗi.

12.11 OWNER CẦN QUYẾT (gật hoặc lắc)

  1. Hai route khác vẫn client_max_body_size 0: claude-kb-route.conf (3 chỗ) và gpt-mcp-route.conf (2 chỗ) — cùng lỗ hổng vừa vá cho claude-mcp, nhưng nằm ngoài phạm vi vòng này nên tôi không tự đụng. Đề xuất: gật cho tôi đặt claude-kb = 50m (cùng lý do, cùng cổng config-guard); còn gpt-mcp là route của đầu nối GPT → hỏi Codex/GPT trước khi đổi.
  2. Chứng tích thử của vòng 2: /root/v2-bodytest/ 63 MB (chỉ là file toàn chữ 'a' để thử 413) + /root/v2-keyrotate/ (khoá mới, phải giữ 600) + các bản .bak-*. Tôi không xoá gì (luật vòng này). Đề xuất: gật cho xoá riêng /root/v2-bodytest/ (rác thuần), giữ toàn bộ .bak-*/root/v2-keyrotate/.
  3. GC staging chưa được CHỨNG MINH: đo hôm nay 0 file quá 7 ngày, nên không thể nói TTL/GC chạy đúng — chỉ nói được là chưa âm thầm làm đầy đĩa. Đề xuất: đo lại sau 25/09/2026 (khi lô file hôm nay quá 7 ngày); nếu lúc đó uploads/results vẫn giữ file cũ thì mới là nợ thật.
  4. Idempotency phía GPT: Đề xuất: để Codex làm theo bản bàn giao ở docs/WORKSPACE_TOOLS.md; tôi tiếp tục không đụng agent-data.
  5. Chứng tích thử tiếp tục tích tụ trên main (_luu-tru-chung-tich-claude/v2-20260918/, 2 commit vòng 2). Đề xuất: giữ; hợp đồng §7 nay đã ghi rõ dọn dẹp = rename, không xoá.

12.12 Đã đổi những gì

  • GitHub Huyen1974/incomex-workspace (main): e11c2a1 (cờ đang-chỉnh trên AGENTS.md, đóng cửa sổ tranh chấp) → f21132a (hợp đồng v1.1 + AGENTS.md cửa vào, một fs_transaction = một commit) → 43d3f84 (chạy lại ca giao dịch 3 file để chấm bộ đếm).
  • Repo VPS /opt/incomex (nhánh feat/s177-sprint1-round-a, không remote): 5d821b8evidence/v2-snapshot-lock-20260918/ (bản TRƯỚC/SAU của script + số đo).
  • Repo agent-data (nhánh codex/mcp-hardening-20260918, không push): 23214cbchỉ docs/WORKSPACE_TOOLS.md.
  • File hệ thống: /opt/incomex/docker/nginx/secrets/claude-mcp-route.conf (3 lần qua cổng config-guard: 32m → xoay khoá → 50m), /opt/incomex/claude-mcp/.env (qua cổng), /opt/incomex/claude-mcp/.deploy-secrets (ngoài registry, sửa tay + backup), /usr/local/bin/mcp-writes-git (ngoài registry — đã đếm = 0 trong registry.tsv, nên không sinh drift mới), /root/mcp-c5-stage/p09-test.py.
  • Bản chuẩn config-guard khai lại cho kb-server-py, kb-compose, mcp-compose (nội dung candidate y hệt bản sống — khai lại, không sửa nội dung). Ghi chú kỹ thuật: validator DOCKER_COMPOSE_CONFIG không chạy được ở thư mục candidate vì thiếu .env cạnh file; tôi đặt một .env rỗng trong thư mục thay đổi để validator chạy đượckhông nới luật: bước kiểm bản SỐNG (docker compose config -q trên đúng thư mục) vẫn chạy đủ và PASS, và hai compose không dùng nội suy ${VAR} nào.
  • Dịch vụ: nginx -s reload ×3 (do chính config-guard chạy, nginx -t sạch trước và sau mỗi lần); incomex-claude-mcp tạo lại 1 lần (bắt buộc — env_file chỉ đọc lúc tạo container, docker restart sẽ giữ khoá cũ), healthy sau 9s; incomex-claude-kb được config-guard chạm qua post-action allowlist (không đổi nội dung). Không đụng: agent-data, Directus, PostgreSQL, Qdrant, Nuxt, cowork-*.

12.13 Đường lùi từng lớp (thứ tự ngược)

  1. Tài liệu kho chung: git revert f21132a (và e11c2a1) trên main.
  2. Bàn giao Codex: docs/WORKSPACE_TOOLS.md.bak-20260918T100241Z hoặc git revert 23214cb.
  3. Bộ đếm test: /root/mcp-c5-stage/p09-test.py.bak-20260918T095922Z.
  4. Snapshot: cp -p /usr/local/bin/mcp-writes-git.bak-20260918T095258Z /usr/local/bin/mcp-writes-git (không cần restart gì — oneshot theo timer).
  5. Bản chuẩn config-guard: /var/lib/incomex-config-guard-v0/rollback/<target>.<mutation-id>.pre (guard tự lưu trước mỗi lần áp).
  6. Khoá + giới hạn body: chép claude-mcp-route.conf.bak-20260918T094608Z (bản khoá CŨ + 0) và .env.bak-20260918T094608Z + .deploy-secrets.bak-20260918T094919Z về chỗ cũ → nginx -s reloaddocker compose -f /opt/incomex/claude-mcp/docker-compose.claude-mcp.yml up -d --no-build --no-deps --force-recreate claude-mcp. Lưu ý: lùi bằng cách chép tay sẽ sinh drift cho config-guard — lùi xong phải khai lại bản chuẩn qua incomex-config-apply-v0.
  7. Khoá mới nằm ở /root/v2-keyrotate/newkey (chmod 600). Chưa bao giờ in ra terminal, log, báo cáo hay commit.

12.14 Còn OPEN thật sự

  • N15 / GPT idempotencyĐÓNG 18/09/2026 (vòng 3, xem §12.16): operation_id đã chạy thật cho cả 5 tool ghi phía GPT.
  • N17 / Claude Chat + Cowork chưa chấm độc lập: C5 KB, C1 (fs_stat/fs_copy/fs_transaction), C2 search cursor, C3 (ui_inspect/ui_act), readiness. Tất cả đều PASS backend + PASS qua cầu nối Claude Code. Phiên mới của Fible cần "làm mới công cụ" để thấy đủ 23 tool — đây là OPEN phía client, KHÔNG phải backend FAIL.
  • N16 / ca cross A/B GitHub hai chiều cần GPT ghi thật — Fible + GPT.
  • N19 upload nhị phân từ Claude Chat / download ra client; N20 profile đăng nhập cho ui_act — đều chờ nhu cầu thật.
  • Hai route còn client_max_body_size 0 — vòng 3: route KB đã đặt 50m (đóng); route GPT rà xong, có kết luận + đề xuất, chờ Codex/GPT xác nhận (§12.16, §12.18). GC staging chưa chứng minh — hẹn đo lại sau 2026-09-25 (§12.16).
  • cloud-init.servicesystemd-networkd-wait-online.serviceĐÓNG 18/09/2026 với kết luận VÔ HẠI / HOÃN kèm bằng chứng (§12.17).
  • N18 ĐÓNG (khoá đã xoay). N22 ĐÓNG (client_max_body_size = 50m). Drift 3 mục ĐÓNG (MISMATCH=0).

12.15 Chờ ai chấm độc lập

  • Fible / Claude Chat (phiên MỚI): gọi thật fs_stat, fs_copy, fs_transaction, fs_search có cursor, ui_inspect, ui_act, vps_status(muc='readiness'), và đọc chính báo cáo này tới EOF bằng get_document_chunk + kb_result_read. Đọc README.md của kho chung để xác nhận hợp đồng v1.1 đến được client.
  • GPT / Codex: đọc lại docs/WORKSPACE_TOOLS.md (mục bàn giao), rồi ca cross A/B GitHub hai chiều với Claude; sau đó làm operation_id.

12.16 VÒNG 3 — đóng dứt điểm hạng mục kết nối GPT ↔ GitHub ↔ VPS — 2026-09-18 (Claude Code, Opus 5)

Prompt hợp nhất (nền GPT + 8 chỗ [CHỈNH] của Fible). Mọi dòng dưới đây gọi thật, không suy từ mã. operation_id phía GPT nay ĐÃ XONG — nợ N15 đóng.

Hạng mục Trước Sau Test thật (backend / Claude Chat / GPT client) Trạng thái
operation_id cho 5 tool ghi OPEN — chỉ có bản bàn giao cho Codex Có trong schema của đúng 5 tool ghi (workspace_edit, workspace_write_new, workspace_copy, workspace_move, workspace_transaction), tuỳ chọn, không tool nào bắt buộc backend 🟢 48/48 ca ngoại tuyến + 32/32 ca HTTP thật có commit+push thật · Claude Chat ⚪ không áp dụng · GPT client ⚪ chờ chủ/GPT tự chấm 🟢 ĐÓNG (backend)
— replay cùng id + cùng nội dung commit đôi trả kết quả cũ, replayed:true, HEAD không nhúc nhích 🟢 gọi thật cả 5 tool 🟢
— trùng id, khác nội dung không có khái niệm OPERATION_ID_REUSED, không file nào đổi 🟢 gọi thật cả 5 tool 🟢
— mất phản hồi có thể commit lần hai client bị cắt thật (curl rc=28, không nhận gì) → máy chủ vẫn commit+push xong → gọi lại cùng id trả đúng commit đó, không commit thứ hai; GitHub xác nhận refs/heads/main 🟢 ca thật, không giả lập bằng hàm nội bộ 🟢
— giao dịch nhiều file 2 file = một commit; replay không sinh commit thứ hai; ca cố tình hỏng việc thứ 3 → không file nào đổi 🟢 gọi thật 🟢
— bền qua restart thao tác xong trước restart vẫn REPLAY sau restart; chốt chặn trùng id cũng sống sót 🟢 5/5 sau một lần restart 🟢
— client cũ không truyền operation_id thì hành vi y hệt cũ 🟢 có ca kiểm riêng 🟢
Chốt dung lượng route GPT client_max_body_size 0 KHÔNG ĐỔI vòng này — đã rà xong, có kết luận + đề xuất, chờ Codex/GPT xác nhận theo đúng [CHỈNH 5] 🟢 đo 63.824 mẫu log: p50=334B, p90=381B, p99=979B, gói hợp lệ lớn nhất 22.110.499 B (~21 MiB) ⚪ OPEN (chờ GPT/Codex)
Chốt dung lượng route KB client_max_body_size 0 50m (chủ đã duyệt) 🟢 request thường qua · 49 MB qua được · 55 MB → 413 trong 0,06 s 🟢 ĐÓNG
Worktree agent-data 1 mục chưa theo dõi (bản lưu 13.579 B) sạch — bản lưu chuyển (không xoá) ra /opt/incomex/backups/agent-data-docs-20260918/, sha256 giữ nguyên 5cacf3e0… 🟢 git status --short rỗng 🟢 ĐÓNG
Hai dịch vụ khởi động FAILED FAILED, chưa rõ nguyên nhân VÔ HẠI / HOÃN — xem 12.17 🟢 đo live: SSH, DNS, HTTPS ra ngoài, 12 container đều chạy 🟢 ĐÓNG (kết luận có bằng chứng)
Hồi quy phía GPT 37 tool vẫn 37 tool, không hụt cái nào 🟢 gọi thật workspace_list/stat/search/read/edit/transaction/result_read/task_start+status/exec+status/ui_inspect; từ chối bản cũ (VERSION_CONFLICT), từ chối chuỗi khớp nhiều chỗ (EXACT_MATCH_REQUIRED), quá tải trả WORKSPACE_BUSY chứ không treo, không force-push 🟢 ĐÓNG
Tốc độ nền cũ sửa+push chậm hơn ~1,3 % (5,008 s → 5,072 s) — trong khoảng nhiễu, xa ngưỡng 2× 🟢 A/B có/không operation_id, 3 lượt mỗi bên 🟢
[CHỈNH 5.2] rác thử vòng 2 /root/v2-bodytest/ 162 MB (5 tệp toàn chữ a) đã xoá đúng thư mục đó 🟢 đã kiểm nội dung là chữ độn trước khi xoá; /root/v2-keyrotate/ và 12 bản .bak-* giữ nguyên 🟢 ĐÓNG
[CHỈNH 5.3] hai cửa chưa ai canh 32 mục 34 mục: thêm hostbin-mcp-writes-gitsecrets-claude-mcp-deploy 🟢 MISMATCH=0; và chứng minh biết kêu: sửa thử mcp-writes-gitMISMATCH CONTENT, khôi phục → CLEAN, sha256 trùng khít 🟢 ĐÓNG
[CHỈNH 5.4] bẫy .env rỗng kiểm bản nháp bằng .env rỗng incomex-config-apply-v0: bản nháp compose có ${thiếu .env thật → từ chối, không kiểm bằng .env rỗng 🟢 ca giả: ${KB_HOSTNAME} + .env rỗng → REJECTED_UNSAFE_CANDIDATE; có .env thật → qua; dịch vụ sống không hề bị đụng 🟢 ĐÓNG
[CHỈNH 8] số tool phía Claude lúc 21, lúc 23 23 — đã sửa mọi chỗ trong §12 🟢 đếm lại live bằng tools/list có bắt tay MCP: 23 (18 cũ + fs_stat, fs_copy, fs_transaction, ui_inspect, ui_act) 🟢 ĐÓNG

Nhánh / commit / image. Theo [CHỈNH 2], không ghi thẳng vào nhánh Codex: nhánh riêng claude/gpt-operation-id-20260918 tách từ codex/mcp-hardening-20260918; e365079 (mã operation_id) + 26bdca3 (tài liệu). Không push, không merge vào nhánh Codex. Codex hoà giải sau bằng git merge claude/gpt-operation-id-20260918 hoặc cherry-pick đúng hai commit đó. Lưu ý sửa một điểm trong prompt: repo agent-data-repo remote (originHuyen1974/agent-data-test), không phải "không có remote" — nhưng vòng này vẫn không push gì lên đó. Image agent-data-hardening:20260918-opid dựng bằng lớp mỏng chồng (Dockerfile.opid, một lớp COPY ~50 KB) chứ không build lại 1,87 GB; image cũ :20260918-final giữ nguyên để lùi; compose lưu docker-compose.yml.bak-pre-opid-20260918. Kho chung: README.md đổi đúng dòng trạng thái operation_id (commit 58ed6905) — ghi bằng chính workspace_editoperation_id.

[CHỈNH 1] Đĩa. Trước build: 96G, dùng 83G, còn 14G (86%). Vì chọn lớp mỏng chồng nên phần tăng thêm ~50 KB chứ không phải 1,87 GB → cổng "còn dưới 10G thì DỪNG" không chạm. Sau build: vẫn 14G trống. Không xoá gì để lấy chỗ, không cài thêm gói nặng.

[CHỈNH 4b] Restart. agent-data restart đúng hai lần, đều sau khi xác nhận không có tác vụ nền / upload dở / phiên GPT đang chạy (13 upload đều committed, 0 bản ghi dở, 0 giao dịch treo): một lần để nạp image mới, một lần dành riêng cho ca thử "bền qua restart" — ca này bắt buộc phải có restart thật mới chứng minh được. Không dịch vụ nào khác bị restart (nginx chỉ reload, vẫn Up 2 weeks). Sau mỗi lần: healthy trong ~1 phút, 37 tool, tools/list đáp trong 0,064 s.

Sổ trạng thái operation_id. state_dir()/operations.jsonl — cùng thư mục với sổ audit sẵn có, tức ngoài mọi gốc workspace, nằm trên bind mount host /opt/incomex/data/workspace-tools nên sống qua restart. Ghi nối tiếp có fsync, quyền 0600; yêu cầu chỉ lưu dưới dạng băm chuẩn hoá (bỏ operation_id trước khi băm). Trạng thái preparingcommitted/applied/rolled_back/unknown. Mã lỗi và ngữ nghĩa đúng bằng phía Claude (OPERATION_ID_REUSED, OUTCOME_UNKNOWN, RECOVERY_REQUIRED) để hai đầu nối cùng một luật (README §5).

Một bẫy đã trả giá trong chính vòng này, ghi lại để đời sau khỏi vấp. Bộ đếm commit đầu tiên của tôi đọc git_log từ kết quả workspace_log — nhưng kết quả đó bị phân trang, nên git_log không có trong trang đầu và bộ đếm luôn trả 0. Mọi khẳng định "không sinh commit thứ hai" khi ấy đều PASS vì lý do sai. Đã chuyển sang đếm thẳng bằng git rev-list --count HEAD trên repo thật và đối chiếu ls-remote với GitHub. ⚠️ Đo bằng chính công cụ đang thử thì phải tính đến phân trang, nếu không phép đo sẽ nói dối theo hướng dễ chịu.

[CHỈNH 8] "2 shells still running". Đó là tiến trình nền của Claude Code trong phiên trước (mấy lệnh theo dõi log nginx), không phải tiến trình trên VPS. Chúng sống theo phiên và đã tắt cùng phiên đó. Đã soát lại VPS vòng này: không còn tail -f / docker logs -f / journalctl -f nào sót. Tiến trình python chạy lâu duy nhất là mow-table2-state-server.py — dịch vụ có sẵn từ trước, không liên quan.

[CHỈNH 5.5] HẸN ĐO LẠI SAU 2026-09-25. Phần tự dọn rác (GC) của staging upload / result tạm chưa chứng minh được vì lô tệp hiện tại chưa quá completed_result_ttl_seconds (7 ngày). Sau 2026-09-25, phiên nào chạm vùng này phải đo lại: đếm tệp trong /opt/incomex/data/workspace-tools/{uploads,results} và xác nhận lô ngày 18/09 đã tự biến mất. Nếu còn nguyên thì GC hỏng thật, không phải "chưa tới hạn".

12.17 Hai dịch vụ khởi động FAILED — kết luận có bằng chứng

Cả hai hỏng đúng một lần duy nhất, tại lần khởi động duy nhất của máy: 2026-02-12 10:16:52 và 10:19:52. Máy chạy liên tục từ 2026-02-12 10:16:41 tới nay (7 tháng 5 ngày, chưa reboot lần nào). Nhật ký hệ thống của lần khởi động đó đã xoay vòng, nhưng log riêng của cloud-init còn giữ nguyên nhân.

  • cloud-init.service — hỏng ở bootcmd trong cloud-config của nhà cung cấp. Ba lệnh, lệnh cuối là pkill -HUP sshd. Lúc bootcmd chạy thì sshd chưa khởi động, pkill không khớp tiến trình nào nên trả mã 1, kéo cả đoạn /bin/sh trả 1 → cloud-init ghi nhận thất bại. Đã đo lại: pkill không khớp → rc=1, đúng cơ chế đó. Lỗi mẫu của nhà cung cấp, không phải lỗi của đầu nối. Các giai đoạn sau của cloud-init chạy 0 lỗi. (Ghi thêm: ý định của bootcmd là bật đăng nhập root bằng mật khẩu; thực tế sshd_config đang là PermitRootLogin prohibit-password — tức an toàn hơn ý định đó, và ta vào bằng khoá nên không ảnh hưởng.)
  • systemd-networkd-wait-online.serviceeth0 ở trạng thái routable (configuring); bản vá netplan bắt chờ eth0 routable, networkd không bao giờ báo "đã cấu hình xong", nên unit hết giờ sau 1 phút 1,9 giây.

Vì sao vô hại — đo, không đoán: network-online.target không có unit nào Requires= nó (chỉ rescue-ssh.target, dùng ở chế độ cứu hộ). After= chỉ là thứ tự, không phải điều kiện, nên mọi dịch vụ xếp sau nó vẫn khởi động bình thường — và thực tế đều đang chạy: docker.service, hermes-*, incomex-mcp-helper, lark-mcp-remote, cùng 12 container. Mạng hiện tại đầy đủ: eth0 có IPv4+IPv6, có default route, DNS phân giải được, curl https://github.com trả 200, SSH đang dùng.

Sẽ tái diễn ở lần khởi động sau không? Gần như chắc chắn , vì cả cloud-config lẫn netplan đều không đổi. Nhưng hậu quả vẫn chỉ là hai dòng FAILED trong bảng, không mất chức năng nào.

Đề xuất (chỉ cần chủ gật/lắc, KHÔNG làm vòng này): để nguyên. Muốn bảng sạch thì sửa trong cửa sổ bảo trì có reboot — đổi pkill -HUP sshd thành pkill -HUP sshd || true, và cân nhắc bỏ Required For Online cho eth0. Không systemctl reset-failed để che: che đi thì lần sau không ai biết nó từng hỏng.

⚠️ Một việc riêng, không thuộc nhiệm vụ này nhưng thấy thì phải báo: cloud-config của nhà cung cấp tại /var/lib/cloud/instance/cloud-config.txt chứa mật khẩu root dạng chữ thường (không chép lại ở đây). Tệp thuộc quyền root. Nếu mật khẩu đó vẫn còn hiệu lực thì nên đổi trong một việc riêng — xin ý kiến chủ.

12.18 Chốt dung lượng route GPT — đã rà, KHÔNG đổi vòng này, kèm bằng chứng

[CHỈNH 5] nói rõ: route KB thì chủ đã duyệt, còn route GPT phải hỏi Codex/GPT trước. Codex hết quota, không hỏi được, nên vòng này chỉ rà và kết luận, không tự đổi cấu hình route sống của GPT.

Hiện trạng đo được. gpt-mcp-route.conf: client_max_body_size 0 kèm proxy_request_buffering off, proxy_buffering off, client_body_buffer_size 1m, timeout 300 s. Khác hẳn route Claude: ở đó 50m là con số dẫn xuất từ FS_WRITE_MAX_BYTES 20 MB × 2 (chữ tiếng Việt escape \uXXXX) = 40 MB < 50m. Phía GPT cố ý không có trần logic ("no arbitrary logical size/count ceilings", Owner 2026-09-18 ghi ngay trong mã).

Kích thước thật, 63.824 mẫu log nginx: p50 = 334 B · p90 = 381 B · p99 = 979 B · gói hợp lệ (status 200) lớn nhất = 22.110.499 B (~21,1 MiB). Hai gói 45 MB/31 MB trong log là rác thử của vòng 2 (status 400) — đã xoá cùng /root/v2-bodytest/.

Vì sao đây là phơi nhiễm thật, không phải lo hão. proxy_request_buffering off nghĩa là nginx không đệm thân request xuống đĩa — rủi ro "đầy đĩa" cổ điển không áp dụng. Nhưng backend đọc toàn bộ thân request vào bộ nhớ bằng await request.json() (server.py, /mcp-gpt-full), không có chốt chặn riêng. mem_limit là 6 GB, đang dùng ~2,1 GB → dư ~3,9 GB. Một request đơn lẻ vài GB sẽ giết bằng OOM đúng incomex-agent-data, tức đường làm việc của ChatGPT. Với client_max_body_size 0, nginx sẵn sàng chuyển tiếp thân request bao nhiêu cũng được.

Vì sao vẫn KHÔNG đổi vòng này. (a) [CHỈNH 5] bắt hỏi Codex/GPT trước; (b) đặt trần sai chỗ sẽ làm hỏng workspace_upload_append — kích thước từng mẩu base64 do client tự chọn, không phải máy chủ quy định; (c) workspace_import_file (luồng đính kèm của ChatGPT) không đi qua route này — bytes được stream thẳng từ phía máy chủ, nên một cái trần ở đây không bảo vệ gì cho luồng đó.

Đề xuất để chủ chỉ cần gật/lắc (một câu): đặt client_max_body_size **64m** cho route GPT — rộng gấp ~3 lần gói hợp lệ lớn nhất từng đo (21,1 MiB), thừa chỗ cho phần phình base64 (×4/3) và JSON-escape tiếng Việt (×2), mà vẫn chặn được một request đơn lẻ làm cạn 3,9 GB bộ nhớ còn lại. Trần logic của tệp không đổi (vẫn không giới hạn: vẫn còn attachment gốc, upload nối tiếp, đọc tiếp). Nếu Codex/GPT nói kích thước mẩu upload của họ có thể vượt 64 MB thì nâng theo số họ đưa, đừng đoán. Gật thì làm trong ~5 phút, có đường lùi, không cần restart (chỉ nginx -s reload qua incomex-config-apply-v0).