KB-5BE0 rev 36

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)

182 min read Revision 36
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" và 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/delete → systemctl 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-conf và env-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-nuxt và incomex-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-20260917T233011Z → compose 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 cũ 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 cũ 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 code → root_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.sh → docker 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" và 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 có 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.env → docker 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ị cũ (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 container ⇒ CHƯ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-20260918T055718Z → compose 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-20260918T070751Z → compose 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.py → systemctl 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 initialize → 200 / 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-* và /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): 5d821b8 — evidence/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): 23214cb — chỉ 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 được — khô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 reload → docker 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.service và systemd-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-git và secrets-claude-mcp-deploy 🟢 MISMATCH=0; và chứng minh biết kêu: sửa thử mcp-writes-git → MISMATCH 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ó ${ mà 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 có remote (origin → Huyen1974/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_edit có operation_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 preparing → committed/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.service — eth0 ở 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 có, 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).


§13. Hardening 2026-09-19 — RUN_ID HARDEN-20260919-01

Người làm: Claude Code (Opus 5). Work order: PROMPT.md @079cdb13643cd96051169dd28fed993a07181bbc trong Huyen1974/incomex-workspace; READY khớp đúng 40 ký tự SHA của commit cuối chạm PROMPT.md (kiểm lại lần hai lúc repo đã tiến lên f65fe21: PROMPT.md không bị chạm sau READY ⇒ READY còn hiệu lực).

Đích của đợt: bỏ bốn loại lỗi "im lặng" — đọc bản cũ mà không biết · từ chối mà không nói lý do · báo thành công giả · ghi đôi. Mỗi lỗi đều tái hiện bằng test đỏ trước khi sửa (§0.4 của work order).

13.1 Bảng trạng thái

Mã Phía Trạng thái Bằng chứng một dòng
H01 Claude 🟢 fs_log/fs_diff nay pull trong cùng một yêu cầu với lượt đọc; kết quả có dòng NGUỒN: đã pull ngay trước khi đọc · source_head=847c231
H02 Cả hai 🟢 đọc quyết định (stat/log/diff) bỏ qua cache; đọc thường trần 2 s cả hai phía (_PULL_TTL_S 20→2, read_refresh_seconds() clamp ≤2)
H03 GPT 🟢 workspace_stat không còn bị loại khỏi bước kéo; worktree bẩn ⇒ worktree_dirty:true + head local + remote_head, refreshed_at:null
H04 GPT 🟢 lỗi git mang phase (fetch · fast-forward · add · diff-check · commit · push · rollback) + event_id tra được trong audit.jsonl; lệnh mạng không hề trả output/URL
H05 GPT 🟢 hai dấu cách cuối dòng + dòng trống cuối file ⇒ THÀNH CÔNG kèm warnings, byte giữ nguyên; <<<<<<</=======/>>>>>>> ⇒ vẫn CHẶN kèm path:line
H06 GPT 🟢 no-op ⇒ changed:false, commit:null, không ghi/không push, HEAD không nhích; hỗn hợp ⇒ files chỉ có file thật sự đổi
H07 Cả hai 🟢 cùng id + payload y nguyên nhưng khác tool ⇒ OPERATION_ID_REUSED nêu tên cả hai tool; trước đó fs_move/workspace_move phát lại kết quả của copy và không hề chạy
H08 Cả hai 🟢 hoàn tác không sạch ⇒ recovery_required (không còn rolled_back/failed), không tái thực thi, nêu rõ file cần kiểm
H09 GPT 🟢 exec kéo mới + ghim source_head trước khi xếp hàng; worker chụp dưới khoá chia sẻ của gốc (cùng inode với đầu nối) và trả source_head THỰC TẾ đã chụp; đo thật: source_head=04a0676, source_head_moved:false, exit 0
H10 GPT 🟢 workspace_diff(from_version) nay đọc được cả bản lưu của transaction (trước đó chỉ thấy bản ghi của edit đơn ⇒ mọi thay đổi nhiều file vô hình)
H11 GPT 🟢 locked() chờ có hạn + backoff, cấu hình resources.lock_wait_seconds; đo thật trên khoá ui DÙNG CHUNG: người giữ 2 s ⇒ đọc chờ 1,9 s rồi PASS; giữ quá hạn ⇒ WORKSPACE_BUSY + retry_after_seconds=5, dừng đúng 5,0 s, không treo. Vòng chờ phía Claude đã kiểm: có sẵn, giữ nguyên
H12 Cả hai 🟢 GPT: CONNECTOR_SCHEMA_VERSION 20260918.1→20260919.1, SERVER_VERSION 1.3.0 thay "1.0.0" cố định ở cả 3 chỗ — đo trên máy chủ ĐANG CHẠY. Claude: SDK không có chỗ đặt serverInfo.version ⇒ công bố qua vps_status(muc='readiness')
H13 GPT 🟢 metrics thêm cột phase + bảng errors theo tool × mã lỗi × phase (trước đó chỉ có một con số tổng)
H14 Cả hai 🟢 GPT: workspace_exec/workspace_task_start nhận operation_id, upload_begin TIẾP TỤC bản dở thay vì bỏ lại bản mồ côi. Claude: đã an toàn sẵn ⇒ ghi bằng chứng bằng 3 ca test, không sửa

13.2 Nghiệm thu backend A–G (§5 của work order)

Ca Trạng thái Bằng chứng một dòng
A độ tươi chéo 🟢 Claude đẩy d5620de; lời gọi GPT đầu tiên ngay sau đó là workspace_stat → thấy đủ 62 B + source_head=d5620de; chiều ngược: GPT đẩy 847c231, Claude fs_log/fs_diff lạnh thấy ngay, fs_diff đọc được commit clone chưa từng có
B snapshot dưới khoá 🟢 ca test: giữ khoá ghi ⇒ snapshot KHÔNG bắt đầu (chờ), thả khoá ⇒ chạy tiếp ⇒ không có trạng thái lai. Đo thật trên hệ đang chạy: workspace_exec kéo mới, ghim source_head=04a0676, worker chụp dưới khoá chia sẻ, trả đúng commit đó, exit 0
C lỗi minh bạch 🟢 C-khoang-trang.md ghi được + 2 warnings (:3 trailing whitespace, :5 new blank line at EOF), byte y nguyên (sha a1f21ac39ee4); C-conflict.txt bị chặn CONFLICT_MARKERS_PRESENT · phase=diff-check · 3 path:line, không để lại file; expected_version cũ ⇒ VERSION_CONFLICT có lý do đầy đủ
D giao dịch 🟢 no-op ⇒ changed:false HEAD không nhích; 2 file ⇒ đúng 1 commit; lỗi ở op #2 ⇒ không file nào được tạo, HEAD không nhích; push bị từ chối ⇒ hoàn tác đủ (hook pre-receive thật, trong bộ test); diff(from_version) sau transaction ⇒ đọc được trước/sau (H10)
E idempotency 🟢 hai phía: cùng id+tool+payload ⇒ replay đúng một commit; payload khác ⇒ từ chối; tool khác ⇒ từ chối; hoàn tác bẩn ⇒ không tái thực thi; mất phản hồi ở điểm tạo việc ⇒ không tạo việc trùng (H14)
F khoá 🟢 giữ ngắn hơn hạn ⇒ đọc tự chờ 1,9 s rồi PASS; giữ quá hạn ⇒ WORKSPACE_BUSY + retry_after_seconds=5 sau đúng 5,0 s, không treo. Đo trên chính file khoá dùng chung với đầu nối Claude (d3ef7de5…lock)
G schema 🟢 hỏi máy chủ ĐANG CHẠY (GET /mcp, không suy từ mã): version=1.3.0 · connector_schema_version=agent-data-hardening-20260919.1 · connector_schema_hash=f6a27f2622ff · 37 tool, trong đó workspace_exec và workspace_task_start đã có operation_id trong schema. Claude: readiness hiện claude-mcp-harden-20260919.1 · 23 tool · vân tay fca7e350ffd2

13.3 Nguyên nhân gốc và chỗ đã sửa

H01 — fs_log/fs_diff đọc clone tại chỗ. app/fsroots.py gọi trợ lý host để đọc sổ git, nhưng khác fs_list/read/stat/search, hai hàm này không có bước _freshen. Sửa ở trợ lý, không phải ở đầu nối: helper/mcp_host_helper.py thêm refresh_for_read() + read_freshness(), và git_log/git_diff/git_head nhận refresh:true ⇒ pull --ff-only rồi đọc trong CÙNG một yêu cầu, nên không ai chen được vào giữa hai bước. Trả thêm refreshed · head · refreshed_at. fsroots.fs_log/fs_diff truyền refresh:true và in _source_note(...) — một dòng nói rõ đọc tại commit nào và có pull được hay không.

H02 — hai bên già đi khác nhau. _PULL_TTL_S mặc định 20 s → 2 s, có trần cứng _PULL_TTL_CAP_S để cấu hình không kéo cao hơn được. fs_stat gọi _freshen(force=True) (trị số version của nó được người gọi đưa thẳng vào fs_edit làm expected_version, nên đây là đọc quyết định). Phía GPT: read_refresh_seconds() clamp giá trị cấu hình vào [0, 2]; SAFETY_READS = (stat, log, diff) luôn kéo mới, bỏ qua TTL.

H03 — workspace_stat bị loại khỏi bước kéo. agent_data/workspace_tools.py, hàm điều phối call() có hai chỗ viết name != 'workspace_stat'. Thay bằng refresh_for_read(name, root, spec): mọi read trên gốc git đều đi qua đó. Worktree bẩn thì không merge đè (prepare_git không chạy) — workspace_stat vẫn trả lời nhưng nói thẳng worktree_dirty:true + head (local) + remote_head + refreshed_at:null; các read còn lại vẫn DIRTY_WORKTREE. Kết quả mọi read git nay mang source_head + refreshed_at.

H04 — mọi lỗi git gom thành một mã. git() cũ: fail('GIT_OPERATION_FAILED', 'Operation failed; …'), bỏ stderr, không ghi log. Nay git_phase(args) suy phase từ chính câu lệnh (không call site nào quên được), mã lỗi cụ thể theo phase (GIT_FETCH_FAILED, GIT_FAST_FORWARD_FAILED, GIT_ADD_FAILED, GIT_COMMIT_FAILED, GIT_ROLLBACK_FAILED…), git_detail() che secret + URL (lệnh mạng thì không trả output), git_event() ghi một dòng git_failed có event_id vào audit.jsonl và chỉ event_id đi ra ngoài. WorkspaceError mang thêm phase/event_id, call() đưa hai trường đó vào phong bì lỗi. Cùng nhóm này: 12 chỗ w.fail(...) trong workspace_operations.py trước đây trả detail RỖNG — đo thật trên đầu nối đang chạy mới thấy (VERSION_CONFLICT ⇒ detail:""). Nay mỗi chỗ nêu path, version trước/sau, và với ROLLBACK_CONFLICT thì nêu danh sách file người vận hành phải kiểm + số hiệu transaction.

H05 — một chốt chặn cho hai việc khác nhau. git diff --cached --check báo lẫn "trailing whitespace" / "new blank line at EOF" với "leftover conflict marker", và cả ba đều làm ghi thất bại. Đây đúng là lỗi đã chặn giao dịch của Host (P05): một dòng Markdown ngắt cứng = hai dấu cách cuối dòng. Nay staged_check() chạy --check với check=False, phân loại từng dòng kết quả: nhóm khoảng trắng → warnings trả về cho người gọi (không chặn, không tự sửa byte); conflict marker → CONFLICT_MARKERS_PRESENT kèm phase=diff-check và path:line. Dùng ở cả write() và transaction().

H06 — changed=bool(after). after chứa mọi path của mọi op, kể cả op cho ra đúng byte đang có. Nay real = {p for p,d in after.items() if d != before.get(p)}: path không thật sự đổi thì không ghi (mtime không nhích), không stage, không commit, không push; changed=bool(real), files chỉ liệt kê real, và no-op có thêm detail giải thích. reset/add khi hoàn tác cũng chỉ chạm real.

H07 — khoá idempotency thiếu tên tool. fs_copy và fs_move (và workspace_copy/workspace_move) nhận bộ tham số giống nhau từng byte, nên khoá chỉ theo payload thì một id dùng cho copy sẽ phát lại kết quả của copy cho move, và move không hề chạy — đo thật: fs_move trả về text của fs_copy, file nguồn vẫn nằm nguyên chỗ cũ trong khi người gọi tin là đã di chuyển. Sửa: so prior['tool'] trước khi so hash, lệch ⇒ OPERATION_ID_REUSED nêu tên cả hai tool. Giữ nguyên hash cũ (không trộn tên tool vào hash) để các id đã ghi trong journal trước đợt này vẫn replay đúng.

H08 — hoàn tác bẩn bị ghi là thất bại sạch. Phía GPT: wrap() bắt mọi WorkspaceError và ghi status='rolled_back' với chú thích "a clean refusal changed nothing, so the same id stays usable" — nhưng ROLLBACK_CONFLICT nghĩa là hoàn tác không sạch. Đo thật: journal ghi {'status': 'rolled_back', 'error': 'ROLLBACK_CONFLICT'}. Nay có danh sách UNCLEAN (ROLLBACK_CONFLICT, RECOVERY_REQUIRED, OUTCOME_UNKNOWN, GIT_ROLLBACK_FAILED, OPERATION_FAILED) → ghi recovery_required kèm paths (rút từ path/from/to/operations), và replay_or_none từ chối id đó bằng RECOVERY_REQUIRED nêu đúng file cần kiểm. find() cho recovery_required quyền vượt trên dòng rolled_back muộn hơn. Phía Claude: fs_transaction ghi status='failed' dù undo_problems khác rỗng (dòng ~1638). Nay problems khác rỗng ⇒ journal recovery_required + ToolError('recovery_required', …) nói rõ "trên đĩa đang là trạng thái LAI" và liệt kê file; _run_write không còn hạ cấp trạng thái đó xuống failed.

13.3b Nguyên nhân gốc và chỗ đã sửa — P1/P2

H09 — snapshot của exec không kéo mới, không giữ khoá. scripts/workspace-exec-worker.py đi thẳng vào os.walk(source) rồi shutil.copy2, nên (a) cây có thể cũ hơn remote và (b) một giao dịch nhiều file đang ghi dở sẽ bị chụp nửa vời. Chia làm hai chỗ, vì hai tiến trình khác nhau: kéo làm ở đầu nối (workspace_execution.execute, w.locked(root) + prepare_git) rồi ghim source_head vào chính bản ghi công việc — worker chạy user khác, cho nó chạy git trong clone là đúng cái bẫy quyền file mà §0.3 của work order cảnh báo; chụp làm dưới cùng file khoá đầu nối dùng (read_lock() trong worker tính đường dẫn bằng digest(tên gốc) trên q.base, tức cùng inode — đã đo: cả container và worker đều chạy quyền root, 21a3230e…lock là một file). Khoá bao trọn: đọc HEAD → đo dung lượng → đi cây → sao .git; không bao docker run (không giữ khoá gốc suốt lúc job chạy). Kết quả trả source_head (commit THẬT đã chụp), source_head_at_queue và source_head_moved. Giữ quá hạn chờ ⇒ xếp hàng lại, không bao giờ chụp một cây lai. Đổi hành vi có chủ ý: workspace_exec trên gốc git nay từ chối khi clone bẩn (prepare_git), giống mọi read và mọi write khác của đầu nối — không thể ghim một commit trên cây bẩn.

H10 — diff(from_version) không thấy giao dịch. Bản gốc (before-image) nằm ở hai chỗ: một file <id>.before cho mỗi edit đơn, và một thư mục transactions/<tid>/<index> cho mỗi giao dịch. diff() chỉ quét chỗ thứ nhất ⇒ mọi thay đổi nhiều file vô hình với diff. Nay quét cả hai và chọn bản mới nhất; manifest lưu before là digest trần còn from_version của gốc git là head:digest, nên so bằng phần sau dấu :. VERSION_NOT_RETAINED nay nói rõ vì sao.

H11 — locked() thử một lần rồi BUSY. README luôn hứa "chờ ngắn rồi trả BUSY", nhưng mã gọi flock(..., LOCK_NB) đúng một lần. Nay chờ tới resources.lock_wait_seconds (mặc định 5 s) với backoff 0,05→0,5 s (không busy-spin), rồi WORKSPACE_BUSY kèm retry_after_seconds và lý do nêu cả ba người có thể đang giữ khoá. WorkspaceError mang thêm retry_after nên con số đi ra ngoài là số thật, không còn là 1 cứng. Không có chỗ nào lồng locked() trong locked() — đã soát cả 5 điểm gọi.

H12 — client giữ danh sách tool cũ. serverInfo.version phía GPT bị ghim "1.0.0" ở ba chỗ suốt năm lần đổi schema, nên một client cache danh sách cũ không có cách nào biết. Nay SERVER_VERSION một chỗ, CONNECTOR_SCHEMA_VERSION nâng cùng commit, kèm comment ghi rõ quy trình phát hành (nâng version → dựng lại → làm mới client: ChatGPT Refresh app, Claude ngắt/kết nối lại connector + chat mới → kiểm tools/list, không tin suông). Phía Claude, FastMCP.__init__ của SDK này không có tham số version (đã kiểm chữ ký hàm), nên thay vì vá SDK hay đổi tên dịch vụ, bản dựng được công bố trong vps_status(muc='readiness'): BUILD_ID + số tool + vân tay hash của (tên + mô tả) mọi tool, kèm một câu nói thẳng "số liệu lệch = client đang giữ danh sách cũ". Không khai tools.listChanged: máy chủ không thật sự gửi thông báo đó, khai ra là hứa hão.

H13 — metrics chỉ đếm tổng. Bảng metrics có cột error nhưng metrics() chỉ trả sum(error IS NOT NULL). Thêm cột phase (có ALTER TABLE bảo vệ để bảng cũ giữ nguyên dữ liệu), call() ghi result.get('phase'), và metrics() trả thêm errors nhóm theo tool × mã lỗi × phase kèm last_at.

H14 — mất phản hồi ở điểm tạo việc. Rà bốn điểm:

  • workspace_exec và workspace_task_start sinh id ở máy chủ: mất phản hồi ⇒ người gọi không có job_id/task_id ⇒ việc thành mồ côi mà vẫn chạy, và một lần gọi lại tạo việc thứ hai. Nay cả hai nhận operation_id: trả lời lần gọi lại trước khi làm bất cứ gì (kể cả trước bước kéo), cùng payload ⇒ trả đúng việc đã xếp hàng (replayed:true), khác payload ⇒ OPERATION_ID_REUSED.
  • workspace_upload_begin cũng sinh id ở máy chủ: nay cùng (root, path, sha256, total_bytes) mà bản cũ còn uploading thì TIẾP TỤC bản đó (đúng nghĩa resumable) kèm offset, thay vì bỏ lại một bản mồ côi chờ GC 7 ngày. Nhánh này nằm sau mọi chốt chặn version/FILE_EXISTS.
  • workspace_import_file gọi begin+commit bên trong: đã an toàn sẵn (FILE_EXISTS/expected_version chặn ghi đôi); chỉ tốn công tải lại. Không sửa.
  • Phía Claude: đã an toàn sẵn, KHÔNG sửa (work order H14 cho phép "ghi bằng chứng"). Mọi lượt ghi vào gốc git-push đi qua MỘT yêu cầu tới trợ lý có rid bền trên đĩa; trợ lý trả lời không kịp ⇒ journal unknown kèm rid; lần gọi lại cùng operation_id đối chiếu hostq.answer_for(rid): trợ lý đã làm xong ⇒ trả kết quả, không ghi lần hai; trợ lý từ chối ⇒ chưa đổi gì nên id được phép chạy. Ba ca test khoá đúng ba nhánh đó lại.

13.4 Test mới

Tệp Số ca Nội dung
agent-data-repo/tests/continuation/test_hardening_20260919.py 16 H02–H08 trên git thật + remote thật (clone thứ hai đẩy commit, hook pre-receive từ chối push, git bị monkeypatch để dựng đúng tình huống hoàn tác bẩn); 2 ca chặn "từ chối không nói lý do"
agent-data-repo/tests/continuation/test_hardening_20260919_p1.py 14 H09–H14: snapshot phải CHỜ người ghi nhả khoá (hai luồng, khoá thật); diff(from_version) sau transaction / edit đơn / copy; khoá chờ rồi PASS và BUSY kèm retry_after; version schema (đọc mã nguồn, không import agent_data.server vì import đó dựng client Qdrant); errors theo tool/mã/phase; 4 ca H14
claude-mcp/tests/test_fsroots_hardening_20260919.py 12 H01, H02, H07, H08 phía đầu nối (trợ lý host thay bằng bản ghi lại lời gọi); 3 ca bằng chứng H14; 2 ca H12
claude-mcp/tests/test_helper_freshness_20260919.py 5 H01/H02/H03 ở trợ lý host, git thật + remote thật

Một khoảng trống đã tự lộ ra và đã bịt. Bộ test test_fsroots_hardening_20260919.py thay trợ lý bằng bản ghi lại lời gọi, nên nó chứng minh đầu nối xin làm mới, chứ không chứng minh trợ lý thật sự pull. Trong lượt chạy này một script sửa file phía trợ lý bị dừng giữa đường (assert trượt trước khi ghi ra đĩa) ⇒ sửa đổi phía trợ lý mất, mà toàn bộ test vẫn xanh và commit d14e8e0 chỉ vào được 1 dòng. Phát hiện khi đối chiếu git show --stat với việc mình nghĩ đã làm. Đã thêm test_helper_freshness_20260919.py — git thật, remote thật — để lần sau chính test bắt được, không phải mắt người. Sửa đúng nằm ở e5132db.

Tổng số ca của đợt này: 47 (16 + 14 phía GPT, 12 + 5 phía Claude).

Bộ test cũ: phía Claude xanh hết — tests.test_fsroots_phase09 7/7, tests.test_is_binary 7/7 (hai module Claude phải chạy riêng từng module; chạy chung một tiến trình thì hỏng do allowlist đặt lúc import — lỗi có sẵn từ trước, không do đợt này). Phía GPT tests/continuation/: 63 xanh / 1 đỏ (64 ca = 34 cũ + 30 mới). Ca đỏ duy nhất là ca CŨ test_background_operation_survives_initial_call, đỏ vì ngân sách chờ — đã truy nguyên ở 13.6b và chứng minh không phải hồi quy của đợt này.

13.5 Commit mã

Trong /opt/incomex/docker/agent-data-repo (đầu nối GPT):

  • a40afbb H02,H03,H04,H05 · workspace_tools.py
  • 04c04b2 H05,H06 · workspace_operations.py
  • 5dfb0fe H07,H08 · workspace_idempotency.py + 14 ca test đỏ-trước
  • 1c98415 H04 · mọi từ chối của transaction phải nói lý do + 2 ca test
  • 2a73e6f H10,H11 · workspace_tools.py (diff đọc bản lưu transaction; khoá chờ có hạn + backoff)
  • 781a0be H13,H14 · workspace_runtime.py (cột phase, bảng errors, find_by_operation)
  • 3b51627 H09,H14 · scripts/workspace-exec-worker.py + workspace_execution.py
  • 62a20b4 H14 · workspace_tasks.py + workspace_transfer.py
  • fa621ea H12 · server.py + 14 ca test P1/P2 đỏ-trước
  • 3a7f9bb · Dockerfile.harden20260919 (công thức dựng overlay)

Trong /opt/incomex (đầu nối Claude; lưu ý repo này có sẵn 174 tệp bẩn không phải của đợt này — mọi commit dưới đây đều git add/git commit theo đường dẫn chỉ định, không add -A):

  • d14e8e0 H01,H02 · trợ lý host — commit thiếu, chỉ vào được 1 dòng (xem 13.4)
  • 43046d1 H01,H02,H07,H08 · app/fsroots.py + 7 ca test
  • e5132db H01,H02,H03 · trợ lý host, bản sửa đủ + 5 ca test git thật
  • 5483e0d H12,H14 · app/hosttools.py (BUILD_ID + vân tay trong readiness) + 5 ca test
  • 015d07d · con trỏ image trong hai compose + claude-mcp/Dockerfile.harden20260919 (xem cảnh báo ở 13.6)

13.6 Dịch vụ đã dựng và deploy

Cả hai container chạy mã trong image (đã kiểm: md5sum mã trong container == mã trên host), nên phải dựng lại image; theo đúng nếp đã có (Dockerfile.phase08, Dockerfile.opid) dùng overlay mỏng một lớp COPY, không cài gói, không cần mạng.

Dịch vụ Image mới Lúc Sức khoẻ Đường lùi
incomex-mcp-helper.service (chạy trực tiếp tệp trên host) 2026-09-19 00:25:40 CEST active /opt/incomex commit 5d821b8
incomex-claude-mcp claude-mcp-local:harden-20260919 (Dockerfile.harden20260919) 00:25:4x healthy < 2 phút image claude-mcp-local:rollback-pre-harden-20260919 (= phase0.9) + docker-compose.claude-mcp.yml.bak-harden-*
incomex-agent-data agent-data-hardening:20260919-harden (Dockerfile.harden20260919) 00:25:48, dựng lại ~00:37 cho 13.3/H04 healthy < 2 phút image agent-data-hardening:rollback-pre-harden-20260919 (= 20260918-opid)

Điều kiện §4 trước khi restart incomex-agent-data đã đo trước khi làm (cả hai lần): hàng đợi job rỗng (không job queued/running/claimed), không manifest prepared/push_unknown/rollback_conflict, không bản ghi ghi-đơn nào ngoài trạng thái kết thúc.

Deploy vòng 2 (P1/P2), 2026-09-19 ~01:30 CEST. Dựng lại cả hai image, restart trợ lý host, và restart incomex-workspace-exec.service. Cả hai container healthy trong < 2 phút; bốn dịch vụ active. Sau khi dựng đã đối chiếu md5sum từng tệp giữa mã trong container và mã trên host — 8/8 và 2/2 khớp.

🔴 Hai điều Host/Owner cần biết về vòng deploy này:

  1. incomex-workspace-exec.service không nằm trong danh sách được phép restart ở §4 của work order, nhưng scripts/workspace-exec-worker.py lại nằm trong phạm vi sửa ở §1, và bản sửa H09 chỉ có hiệu lực sau khi restart. Đã restart với đúng điều kiện áp cho incomex-agent-data (hàng đợi rỗng — đã đo). Nêu ra đây để Host/Owner xác nhận cách hiểu, không phải để lấp liếm.
  2. Overlay Dockerfile.harden20260919 phía GPT ban đầu chỉ COPY 3 tệp (chép theo nếp Dockerfile.opid), trong khi vòng P1/P2 đụng 8 tệp ⇒ lần dựng đầu, 5 tệp sửa không vào image. Bước đối chiếu md5sum container-vs-host bắt được ngay; đã liệt kê đủ 8 tệp rồi dựng lại và đối chiếu lại. Vẫn liệt kê tên tệp tường minh thay vì COPY agent_data/ cả thư mục, để __pycache__ và tệp rác AppleDouble ._* trong cây làm việc không bao giờ lọt vào image.

Con trỏ image trong compose. docker/docker-compose.yml dòng 189 và claude-mcp/docker-compose.claude-mcp.yml dòng 46 được nâng sang tag mới — đây là cách deploy của chính nếp này. Đã commit ở 015d07d cùng hai Dockerfile.harden20260919, để không còn gì nửa chừng. ⚠ Lưu ý cho Host: dòng trong docker/docker-compose.yml đã bẩn từ trước khi đợt này bắt đầu (vòng trước nâng 20260918-final → 20260918-opid mà chưa commit), nên commit 015d07d mang theo cả lần nâng chưa commit của vòng trước; thông điệp commit nói rõ điều đó.

Cách hoàn nguyên nếu cần (§4 cho phép đúng một đường lùi): đổi hai dòng image: về claude-mcp-local:rollback-pre-harden-20260919 và agent-data-hardening:rollback-pre-harden-20260919, docker compose up -d, và git revert các commit [Claude Code] liệt kê ở 13.5 (trong đó scripts/workspace-exec-worker.py cần systemctl restart incomex-workspace-exec.service).

13.6b Một ca test CŨ đang ĐỎ — không phải do đợt này

tests/continuation/test_workspace.py::test_background_operation_survives_initial_call trượt. Đã truy đến cùng thay vì báo suông:

  • Ca đó chỉ chờ 5,0 s (100 vòng × 0,05 s) cho một việc nền. Việc nền phải spawn hai tiến trình con (supervisor rồi worker), mỗi tiến trình import cả gói agent_data.
  • Đo thật trên máy đang tải (load average ≈ 4, do chính các lượt chạy test của đợt này): việc nền chạy ĐÚNG — status=completed, nội dung trả về chính xác — nhưng mất 11,8 s, tức quá ngân sách 5 s.
  • A/B để loại trừ đợt này: lấy workspace_tasks.py nguyên bản từ git show HEAD chạy 3 lượt → trượt cả 3; bản của đợt này chạy 3 lượt → cũng trượt cả 3, thời gian như nhau. ⇒ không phải hồi quy của đợt này. (Lần bisect đầu tiên chỉ chạy 1 lượt và tình cờ xanh, suýt dẫn đến kết luận sai; ghi lại để đừng tin một lượt chạy.)
  • Không sửa ca test đó: nó không thuộc phạm vi work order (§6 cấm mở rộng). Đề nghị cho Host: nới ngân sách chờ của ca này, hoặc cho nó chờ theo tín hiệu thay vì đếm vòng.

13.7 Việc còn chờ

  • Chờ nghiệm thu ở client thật (không phải việc của Agent — §8 của work order): làm mới hai client theo quy trình phát hành, rồi Claude Chat kiểm fs_log/fs_diff lạnh + fs_transaction + operation_id; GPT Chat kiểm gọi thẳng workspace_edit/workspace_transaction có operation_id, workspace_stat lạnh, và cảnh báo khoảng trắng. Chưa có gì được gọi là "production ready" khi client chưa PASS.
  • Một dòng để Host biết: COLLAB.md mục Prompt vẫn ghi Owner giao chạy: chưa. Owner đã ra lệnh RUN trực tiếp cho Agent trong phiên này; §6 cấm Agent sửa COLLAB.md nên dòng đó để Host cập nhật.

13.8 Chứng tích thử trong repo chung

Toàn bộ commit thử nằm trong _thu-nghiem/hardening-20260919/, không xoá (§5), tác giả "Claude via MCP" (phía Claude) hoặc "AI via Incomex Workspace" (phía GPT):

d5620de · 847c231 (A, độ tươi chéo) · 4510fb0 · d03153a (C, khoảng trắng + version cũ) · a0ec25e · 11b5fc1 · d37cb68 (D, giao dịch) · a044f66 · f42248e (E, idempotency phía GPT) · ce486ef · 04a0676 (E, idempotency phía Claude).

10 tệp: A-do-tuoi-cheo-claude.txt · A-do-tuoi-cheo-gpt.txt · C-khoang-trang.md · D-mot.txt · D-hai.txt · D-that-su-moi.txt · E-idem.txt · E2-copy.txt · E-claude-idem.txt · E-claude-copy.txt. AGENTS.md · README.md · COLLAB.md · PROMPT.md không bị chạm (kiểm bằng git log 34371d8..origin/main -- <4 tệp>: chỉ có hai commit của Host, không có commit nào của Agent).

13.9 K10 — journal operation_id sống qua TIẾN TRÌNH MỚI (RUN K10-PERSIST-20260919-02)

Người làm: Claude Code (Opus 5). READY kiểm trên bản sao riêng dưới /tmp: READY@83b3a07749d79f84481723dddb72619d70b4d9ee khớp commit cuối chạm PROMPT.md; kiểm lại lần hai lúc repo đã tiến lên 2fc3cf6 (P09/P10) — PROMPT.md không bị chạm sau READY ⇒ READY còn hiệu lực. Không restart production chỉ để thử; không sửa runtime.

13.9.1 Bằng chứng (a) — journal nằm ngoài mọi gốc, trên bind mount của host

Đo bằng docker inspect trên container ĐANG CHẠY, không suy từ compose:

Phía Journal / state Bind mount host → container Gốc có chứa nó?
Claude FS_OPS_JOURNAL=/var/log/mcp-audit/fs-ops.jsonl /var/log/mcp-audit → /var/log/mcp-audit rw=true không (gh · ui · docs · code)
GPT state_dir=/workspace/state ⇒ operations.jsonl và queue.sqlite /opt/incomex/data/workspace-tools → /workspace/state rw=true không (ui · docs · workspace=/workspace/state/github-workspace · agent-data)

docker-compose.yml phía GPT là 0600 nên không đọc được qua gốc code — đó là lý do dùng docker inspect chứ không đọc compose. Phía Claude compose đọc được (dòng 88 + dòng 115).

Đã sống qua restart thật, không chỉ đúng cấu hình. Container hiện tại khởi động incomex-claude-mcp 2026-09-18T23:13:54Z · incomex-agent-data 2026-09-18T23:16:38Z; dòng ĐẦU TIÊN còn trong journal là 2026-09-18T07:12:13Z (Claude) và 2026-09-18T12:05:00Z (GPT) — tức cũ hơn lần khởi động ~16 h và ~11 h. Journal đã đi qua restart, không phải nằm trong lớp ghi của container.

13.9.2 Bằng chứng (b) — tiến trình A ghi → A CHẾT → tiến trình B MỚI phát lại

Hai bộ test độc lập, mỗi lượt gọi là một python3 riêng (subprocess.run đợi, nên lượt sau chắc chắn bắt đầu khi lượt trước đã chết); chỗ duy nhất hai lượt chia sẻ là file journal / state tạm. Lý do phải là tiến trình thật: cả hai đầu nối đọc lại journal từ đĩa mỗi lần gọi, nên một ca cùng tiến trình vẫn xanh dù trạng thái chỉ nằm trong RAM — nó không chứng minh được điều K10 cần.

Test Đường dẫn Ca Kết quả
Claude claude-mcp/tests/test_fsroots_persistence_k10_20260919.py 8 🟢 Ran 8 tests · OK
GPT agent-data-repo/tests/continuation/test_persistence_k10_20260919.py 8 🟢 8 passed

Chạy lại: cd /opt/incomex/claude-mcp && python3 -m unittest tests.test_fsroots_persistence_k10_20260919 -v docker run --rm -v /opt/incomex/docker/agent-data-repo:/src:ro -v /opt/incomex/config/workspace-tools.json:/prod-config.json:ro -e K10_PROD_CONFIG=/prod-config.json -w /src --entrypoint python3 agent-data-hardening:20260919-harden -m pytest tests/continuation/test_persistence_k10_20260919.py -o addopts= -q

Bốn điều kiện PASS, đo được từng cái:

  • phát lại: B nhận [REPLAY operation_id=…] (Claude) / replayed:true + first_performed_at (GPT), kèm ĐÚNG commit/job_id của A;
  • không làm lần hai: phía Claude B không gọi gh_put_commit_push và journal chỉ có MỘT dòng done; phía GPT rev-list --count HEAD không tăng, remote không nhích, MARKER-K10 xuất hiện đúng 1 lần, và workspace_exec/workspace_task_start chỉ có đúng 1 job/task cho một operation_id;
  • cùng id + payload khác bị từ chối: operation_id_reused (Claude) / OPERATION_ID_REUSED (GPT), và không ghi gì. Phía Claude còn khoá cả H07 qua ranh giới tiến trình (id của fs_copy không dùng cho fs_move);
  • phát lại KHÔNG ghi lại file: gốc cục bộ — sau khi A ghi, một tay khác đổi file, B phát lại thì nội dung giữ nguyên bản đã bị đổi.

13.9.3 Test có thật sự bắt lỗi không — đối chứng ÂM và đột biến

Không tự nhận "xanh nghĩa là đúng". Ba phép đo:

  1. Đối chứng ÂM nằm SẴN trong cả hai bộ test (ở lại vĩnh viễn): xoá journal → tiến trình mới phải thực hiện thật (Claude: có gh_put_commit_push, hết [REPLAY]); xoá bảng jobs → workspace_exec cấp job_id MỚI. Không có ca này thì "B không ghi" chưa chứng minh được là nhờ đọc journal cũ.
  2. Đột biến 1 (/tmp/k10-mutate, bản sao cô lập, KHÔNG chạm production): cho _journal_find luôn trả None ⇒ 5/8 ca ĐỎ.
  3. Đột biến 2 (/tmp/k10-mutate2): đặt journal BÊN TRONG một gốc + compose bỏ dòng mount ⇒ 2/8 ca ĐỎ.

Ca thứ 8 (test_journal_path_comes_from_the_environment) là kiểm hợp đồng đơn giản, không kỳ vọng bắt lỗi.

Nói thẳng về sức nặng của từng khẳng định (đo chứ không đoán): phía GPT mọi công cụ ghi đều đã có chốt version/tồn-tại, nên một lần chạy lặp KHÔNG thể commit hai lần — nó trả VERSION_CONFLICT/FILE_EXISTS. Vì vậy ở đường GHI phía GPT, khẳng định gánh trọng lượng là "B nhận đúng kết quả ĐÃ GHI thay vì lỗi khó hiểu đó"; còn "không sinh lần hai" gánh trọng lượng thật ở đường JOB (không có gì ngoài sổ hàng đợi ngăn một retry xếp việc lần hai) và ở đường ghi phía Claude (fs_write lên gốc gh không cần expected_version nên chạy lại là push thật — đối chứng ÂM đã cho thấy đúng như vậy).

13.9.4 Hồi quy liên quan của cả hai đầu nối

  • Claude — 39/39 🟢, chạy một module một tiến trình: k10 8 · hardening_20260919 12 · phase09 7 · helper_freshness 5 · is_binary 7. ⚠ Phát hiện có sẵn, không do đợt này: các module test phía Claude đặt biến môi trường ở cấp module nên không chạy chung một tiến trình được — gộp cả 5 module cho 15 ca đỏ GIẢ. Kiểm bằng cách gộp 4 module CŨ (không có module mới): vẫn 31 tests, 8 failures. Đề nghị cho Host, ngoài phạm vi work order: tách env ra fixture hoặc khai báo rõ "một module một tiến trình" trong runbook.
  • GPT — 72/72 🟢 trong một lượt pytest tests/continuation/ (426,8 s): caps 2 · hardening 10 · hardening_20260919 16 · hardening_20260919_p1 14 · k10 8 · workspace 22. ⚠ Ca nợ ở 13.6b (test_background_operation_survives_initial_call) XANH trong lượt này. KHÔNG coi là đã trả nợ: lượt này chạy trong container docker run --rm không giới hạn CPU, tức ngân sách thời gian khác production — đúng nguyên nhân đã truy ở 13.6b. Nợ vẫn để nguyên cho Host.

13.9.5 Có sửa runtime không · commit

KHÔNG sửa runtime. Test K10 xanh ngay từ đầu, nên theo §5 của PROMPT không dựng image, không deploy, không restart. Kiểm chứng: NRestarts=0 và StartedAt của cả hai container không đổi (23:13:54Z / 23:16:38Z), ActiveEnterTimestamp của incomex-mcp-helper.service và incomex-workspace-exec.service không đổi, 48 image không thêm không bớt. Cả hai container healthy. Lượt chạy test dùng container dùng-một-lần docker run --rm từ image có sẵn, nguồn mount :ro.

Chưa commit — cố ý. PL1 giới hạn việc git trong bản sao riêng dưới /tmp, nên Agent không commit trên VPS. Hai tệp đã ở trên đĩa, Host commit bằng một lệnh:

  • /opt/incomex/claude-mcp/tests/test_fsroots_persistence_k10_20260919.py sha256 c9e8fd1d1320eaf93fb264d5c6e7d01bc12efea8d02dcdd59d4df2aa4504b641 (nằm trong repo /opt/incomex, nhánh feat/s177-sprint1-round-a, 171 mục bẩn có từ trước — nếp cũ là stage đúng tệp cần, không commit cả cây)
  • /opt/incomex/docker/agent-data-repo/tests/continuation/test_persistence_k10_20260919.py sha256 cbf5e7567d833bb0e2283e40b263cff35fdd3a2d662803d7f2d8c63a184d555c (nhánh claude/gpt-operation-id-20260918, đang ?? untracked)

13.9.6 Tự kiểm Phụ lục P10 (PL1–PL3) — KHÔNG thấy vi phạm

Host yêu cầu đọc lại P10 giữa lượt chạy và ghi bằng chứng. Đã rà từng việc đã làm:

  • PL1 · git chỉ trong bản sao /tmp. Clone + fetch chỉ ở bản sao riêng của phiên dưới /tmp. Trên VPS không fetch/pull/checkout/commit ở bất cứ đâu. Trong /opt/incomex/docker/agent-data-repo (KHÔNG thuộc hai clone bị nêu tên) chỉ chạy git chỉ-đọc: log, status --short, rev-parse; .git vẫn duy nhất root:root. Hai clone bị nêu tên: git của root không mở được /opt/incomex/mcp-roots/gh ("dubious ownership", Agent KHÔNG thêm safe.directory), chủ sở hữu vẫn duy nhất incomex-mcp-helper:incomex-mcp-helper; hoạt động 01:14–01:21Z ở data/workspace-tools/github-workspace là của GPT Chat — reflog ghi chính commit 2fc3cf6 "[GPT] P09,P10 · COLLAB.md", không phải của Agent.
  • PL2 · chỉ sửa runtime khi test đỏ. Test xanh ⇒ không sửa, không dựng, không restart (số liệu ở 13.9.5). Không cần tag rollback vì không dựng gì.
  • PL3 · không xoá, không đổi auth/URL/secret, test chỉ dùng state tạm. Không xoá file/dữ liệu/image/nhánh nào của hệ thống; chỉ tạo rồi giữ lại hai thư mục nháp của chính Agent (/tmp/k10-mutate, /tmp/k10-mutate2 — để lại làm chứng tích đột biến). Không chạm auth/URL/secret. Test không ghi vào journal thật — bằng chứng đo được: grep -c 'k10-' = 0 ở CẢ HAI journal thật; operations.jsonl phía GPT y nguyên (19351 B · 52 dòng · mtime 2026-09-19T00:37). Hai dòng mới trong journal Claude lúc 00:59:29Z và 01:10:58Z là fs_edit root=gh với id tự sinh op-9f0f44c39fc2136a / op-cedf0a9755269a7b — đó là Claude Chat sửa COLLAB.md, không phải Agent.

13.9.7 Kết luận và việc còn chờ

K10: phần của Agent PASS ở CẢ HAI phía — bind mount host đã xác minh trên container đang chạy, journal đã đi qua restart thật, tiến trình MỚI phát lại đúng từ journal cũ, không sinh commit/job/write lần hai, cùng id + payload khác bị từ chối, và hồi quy liên quan xanh (39/39 · 72/72).

Không tuyên bố gì về client. K1–K9 vẫn là việc của Claude Chat / GPT Chat, và ba lượt L1→L2→L3 của P08 chưa chạy. Agent không chạm AGENTS.md, README.md, COLLAB.md, PROMPT.md.

13.10 R02 đóng tầng máy — RUN R02-CLOSEOUT-20260919-03

Người làm: Claude Code (Opus 5). READY kiểm trên bản sao riêng dưới /tmp (git clone kho công khai, không đụng hai clone làm việc): READY@8537a8e3a073af9b623047984a5f253af039640f khớp đủ 40 ký tự với commit cuối chạm PROMPT.md; kho lúc kiểm ở c4d7f00. Áp PL1–PL3 theo đính chính 7159559: PL1 chỉ cấm git-ghi trên /opt/incomex/mcp-roots/gh và /opt/incomex/data/workspace-tools/github-workspace; repo mã được commit cục bộ, không push.

13.10.1 Bảng bốn tầng

Tầng Trạng thái Ai Bằng chứng
CODE 🟢 PASS Agent K10 + runner đã commit: Claude ee76fd5, GPT f3f4c41. Hồi quy bằng chính lệnh chuẩn: Claude 39/39, GPT 72/72
SERVER 🟢 PASS Agent tools/list qua đúng route công khai của client: Claude 23 tool · vân tay fca7e350ffd2 · GPT 37 tool · 1.3.0 · agent-data-hardening-20260919.1+b1gdc+workspace-20260918 · hash f6a27f2622ff. Cả 9 năng lực bắt buộc đủ ở hai phía, 0 BLOCK
CLIENT ⏳ PENDING Claude Chat · GPT Chat K1, K2, K9 — chưa chạy. Agent không tuyên bố gì ở tầng này
CROSS ⏳ PENDING hai chat K3–K8 — chưa chạy (ba lượt L1→L2→L3 của P08)

Không dòng nào ở đây được đọc là "production ready". Tầng máy hết nợ; hai tầng còn lại chưa đo.

13.10.2 Chứng tích K10 đã vào repo mã (bước 2)

  • Claude — /opt/incomex commit ee76fd5: claude-mcp/tests/test_fsroots_persistence_k10_20260919.py
    • claude-mcp/tests/run_acceptance.py. Stage theo đúng hai đường dẫn; số tệp bẩn 173 → 171, tức 171 tệp bẩn có sẵn không bị đụng. Không push.
  • GPT — /opt/incomex/docker/agent-data-repo commit f3f4c41: tests/continuation/test_workspace.py
    • tests/run_acceptance.py. Cây làm việc sau commit sạch. Không push. Tệp K10 tests/continuation/test_persistence_k10_20260919.py đã nằm trong commit 3955c6c từ trước (xem cảnh báo ngay dưới); sha256 cbf5e756…d555c khớp bản trên đĩa.

🔴 Một việc Host phải biết, Agent không gây ra nhưng đo được. 3955c6c không phải commit của người: nó do cron sao lưu hằng ngày /opt/incomex/scripts/git-push-gh-daily-v2.sh tạo lúc 04:00 UTC 19/09. Cron đó, trên /opt/incomex/docker/agent-data-repo: thấy cây bẩn ⇒ git add -A ⇒ commit "auto-snapshot: N files" ⇒ git branch -f vps-daily-<ngày> ⇒ git push --force lên Huyen1974/agent-data-test. /var/log/git-push-gh.log ghi đúng ba bước đó. Hệ quả: (a) chứng tích K10 đã rời VPS lên GitHub ngày 19/09 ở nhánh vps-daily-20260919 — không do Agent, nhưng là sự thật cần ghi; (b) bất cứ tệp nháp nào còn bẩn lúc 04:00 hoặc 16:00 UTC đều bị gom vào một commit chung rồi force-push. Repo /opt/incomex (phía Claude) không nằm trong cron này, nên 171 tệp bẩn ở đó an toàn. Sau f3f4c41 cây agent-data-repo đã sạch nên mốc 16:00 UTC không có gì để gom. Đề nghị cho Host (ngoài phạm vi lượt này, Agent không tự sửa): cho cron stage theo đường dẫn thay vì add -A.

13.10.3 Test-harness đáng tin (bước 3)

Claude — nguyên nhân gốc của 9 đỏ giả, bỏ được mà không đụng runtime. Mỗi module test tự ghim hộp cát của nó bằng os.environ[...] ở cấp module, trong khi app/fsroots.py (dòng 93–95: _WRITE_LOG, _OPS_JOURNAL, _LOCK_DIR) và app/hostq.py (dòng 27–28: _QUEUE_IN, _QUEUE_OUT) đọc các biến đó vào biến toàn cục lúc import. Module nào import trước thì thắng cả tiến trình; module sau chạy trên hộp cát không phải của mình. Đo trước khi sửa: gộp 5 module trong một tiến trình ⇒ Ran 39 tests · FAILED (failures=5, errors=4) — 9 đỏ giả; một module một tiến trình ⇒ 39/39 xanh. Sửa: không đổi một dòng runtime nào, chỉ thêm claude-mcp/tests/run_acceptance.py chạy mỗi module trong một tiến trình riêng. Chứng minh đúng tiêu chí: 3 lần chạy liên tiếp cho kết quả GIỐNG NHAU TỪNG BYTE (sha256 cả ba lượt a4d0f1f26798178e), 39/39 xanh, 0 đỏ giả, trong lúc máy đang tải 5.16.

GPT — bỏ phụ thuộc may rủi tải máy ở test_background_operation_survives_initial_call. Bản cũ chờ đúng 5,0 s (100 vòng × 0,05 s) rồi assert — ngân sách bấm giờ, nên máy bận là đỏ (đúng nợ §13.6b). Bản mới chờ theo trạng thái của chính tác vụ: await_task() hỏi workspace_task_status cho tới khi status vào tập kết thúc {completed, failed, cancelled}, backoff 0,02→0,5 s, kèm trần thời gian hữu hạn WORKSPACE_TASK_WAIT_SECONDS (mặc định 120 s) để một tác vụ treo thật vẫn đỏ chứ không treo bộ test. Trần là chốt chống kẹt, không phải bấm giờ: lớn hơn ~10 lần thời gian tệ nhất từng đo (11,8 s) và nhỏ hơn hạn 300 s của route. Hỏng thì báo đủ status + heartbeat + position.

🔴 Một giả định của vòng trước đã SAI, đo bằng docker inspect trên container ĐANG CHẠY. §13.9.4 viết "lượt này chạy trong container docker run --rm không giới hạn CPU, tức ngân sách thời gian khác production". Sự thật: production incomex-agent-data có NanoCpus=0 · CpuQuota=0 · CpuShares=0 · CpusetCpus=[] — production cũng KHÔNG giới hạn CPU; giới hạn duy nhất là Memory=6442450944 và MemorySwap=12884901888. Vậy "đúng giới hạn container production" = đúng hai trần bộ nhớ đó và không trần CPU.

Chứng minh:

  • Đúng giới hạn production, 20 lượt liên tiếp: 20/20 XANH, thời gian phẳng 17,1–20,4 s mỗi lượt.
  • A/B có đối chứng (cùng image, cùng trần bộ nhớ, cùng bóp CPU --cpus=0.35, container MỚI mỗi lượt, khác nhau đúng một thứ là thân test): thân MỚI 6/6 XANH (66,6–76,4 s) · thân CŨ 0/6 xanh, 6/6 ĐỎ với KeyError: 'result' — đúng kiểu hỏng của ngân sách 5 s hết hạn khi tác vụ còn đang chạy. ⇒ phụ thuộc tải máy đã mất, không phải "lần này may".

⚠ Một cái bẫy trong chính phép đo của tôi, ghi lại để đời sau khỏi vấp. Lần dựng A/B đầu tiên chạy 20 lượt trong CÙNG một container; thời gian mỗi lượt tăng đều 64,9 → 79,5 → 96,1 → 123,6 s. Không phải test chậm đi: soi /proc trong container thấy các tiến trình agent_data.workspace_tasks supervise của lượt trước vẫn sống, cùng chia nhau đúng hạn 0,35 CPU. Đã kiểm production để khỏi kết luận bừa: container incomex-agent-data có 3 supervisor, cả ba cùng ~5,5 giờ tuổi (bằng tuổi container), PPid=1, trạng thái S — tức một bể tiến trình sống lâu quanh task_concurrency=2, KHÔNG phải rò rỉ tăng dần. Kết luận: con số tăng dần là lỗi thiết kế phép đo của tôi, không phải lỗi đầu nối; đã dựng lại A/B với container mới mỗi lượt và thời gian trở lại phẳng. ⚠️ Chạy nhiều lượt việc-nền trong một container bị bóp CPU thì phải tính đến bể supervisor, nếu không phép đo sẽ nói dối theo hướng khó chịu.

13.10.4 Minimum shared capability (bước 4) — đo theo NĂNG LỰC, không theo số tool

Đọc từ tools/list qua đúng route công khai mà client dùng, không hỏi container.

Năng lực Claude (Incomex VPS) GPT (Incomex AgentData) Kết
discover/list fs_list workspace_list 🟢
read/search/stat fs_read · fs_search · fs_stat workspace_read · workspace_search · workspace_stat 🟢
edit/create fs_edit · fs_write workspace_edit · workspace_write_new 🟢
diff/log fs_diff · fs_log workspace_diff · workspace_log 🟢
copy/move fs_copy · fs_move workspace_copy · workspace_move 🟢
multi-file transaction fs_transaction workspace_transaction 🟢
expected version/head expected_version ở fs_write/fs_edit, expected_head ở fs_transaction expected_version ở workspace_edit, expected_head ở workspace_transaction/workspace_write_new 🟢
idempotency operation_id ở 5/5 tool ghi operation_id ở 7/7 tool ghi/tạo việc 🟢
freshness gọi thật: fs_log lạnh trả NGUỒN: đã pull ngay trước khi đọc · source_head=c4d7f00 gọi thật: workspace_stat trả source_head=c4d7f00 + refreshed_at 🟢

0 BLOCK. Không viết tính năng mới, không tạo tool trùng để bằng số lượng (Claude 23 ≠ GPT 37 là đúng thiết kế: mỗi bên có tool riêng ngoài hợp đồng chung).

Ba chỗ lệch nhưng không phải thiếu, ghi để hai chat khỏi suy diễn:

  1. workspace_edit phía GPT bắt buộc expected_version (nằm trong required); fs_edit phía Claude để tuỳ chọn. Cùng năng lực, khác độ chặt.
  2. Chuỗi nạp tệp phía GPT (workspace_upload_begin/append/commit, workspace_import_file) không có operation_id — đúng thiết kế H14: chống ghi đôi bằng khoá tự nhiên (root, path, sha256, total_bytes) và FILE_EXISTS/expected_version, không bằng sổ idempotency.
  3. write_file phía Claude (đường allowlist cũ, ngoài bộ fs_* git) không có operation_id; nó không thuộc hợp đồng chung.

13.10.5 Schema / release gate (bước 5)

Bề mặt sống, đo qua đúng endpoint của client (nginx → container); secret không ghi ở đâu.

Claude — Incomex VPS GPT — Incomex AgentData
protocolVersion 2025-06-18 2024-11-05
serverInfo.name incomex-claude-mcp agent-data-mcp-gpt-full
serverInfo.version 1.27.1 ⚠ là version của SDK mcp, KHÔNG phải bản dựng đầu nối 1.3.0 (đầu nối tự đặt)
version thật của bản dựng claude-mcp-harden-20260919.1 qua vps_status(muc='readiness') connectorSchemaVersion = agent-data-hardening-20260919.1+b1gdc+workspace-20260918
hash/vân tay fca7e350ffd2 connectorSchemaHash = f6a27f2622ff
số tool 23 37
capabilities tools.listChanged = false; resources/prompts listChanged = false; subscribe = false {"tools": {}} — không khai listChanged

⚠ serverInfo.version phía Claude là mồi nhử cho việc chốt phát hành. Đầu nối không đặt trường này ở đâu cả (đã soát app/server.py); 1.27.1 chính là version gói mcp trong container (đo bằng importlib.metadata.version("mcp")). Nó chỉ đổi khi nâng SDK, không bao giờ đổi khi bề mặt tool đổi. Đây là chỗ H12 nói "SDK không có chỗ đặt serverInfo.version" — nói chính xác hơn: trường đó có, và nó nói sai điều người đọc tưởng. Tín hiệu đúng vẫn là BUILD_ID + vân tay trong readiness.

✅ Vân tay phía Claude TỰ KIỂM ĐƯỢC từ phía client — đã chứng minh, không phải hứa: lấy tools/list qua route công khai rồi tính sha256(repr(sorted((name, description[:400]))))[:12] ⇒ fca7e350ffd2, khớp đúng con số đầu nối tự báo. Một chat nghi "mình thấy thiếu tool" có thể tự chứng minh. ⚠ Hash phía GPT thì KHÔNG tự kiểm được từ dây: _mcp_schema_hash() băm MCP_TOOLS nội bộ, mà bản lên dây có thêm _meta/annotations ⇒ tính lại từ tools/list ra 2d925e5d5017, không phải f6a27f2622ff. Hash đó vẫn làm đúng việc của nó (đổi schema thì đổi hash ⇒ client bust cache), nhưng client không dùng nó để tự đối chứng được. Phép kiểm dùng được ở phía GPT: 37 tool + connectorSchemaVersion + có operation_id ở bốn tool đã nêu.

listChanged: cả hai đều KHÔNG hứa, và đó là đúng. Claude khai false tường minh; GPT không khai trường này. Không bên nào thật sự gửi notifications/tools/list_changed, nên không có cơ chế chuẩn nào đẩy schema mới sang client đang mở. Không phát minh workaround riêng. ⇒ Luật phát hành (chốt): đổi schema ⇒ nâng version cùng commit ⇒ dựng lại ⇒ client cũ KHÔNG được dùng để nghiệm thu; cổng phát hành là ngắt/kết nối lại (Claude) hoặc Refresh app (GPT) + MỞ CHAT MỚI, rồi đối chiếu số tool/vân tay trước khi tin.

Phân xử lỗi client GPT đang thấy (transaction bị chặn vì thiếu operation_id): LỖI CLIENT, KHÔNG PHẢI LỖI MÁY CHỦ. Gọi tools/list qua đúng URL công khai ChatGPT dùng (qua nginx, profile gpt-full, không gọi thẳng container) rồi đọc inputSchema thật:

tool operation_id trên máy chủ SỐNG
workspace_edit ✅ có
workspace_transaction ✅ có
workspace_exec ✅ có
workspace_task_start ✅ có

Đủ cả bốn ⇒ theo đúng nhánh PROMPT mục 5 quy định: "client giữ schema cũ", chỉ còn reconnect. Không có test đỏ, không sửa, không deploy cho việc này. (Các fs_* ghi phía Claude cũng đủ operation_id.)

13.10.6 Một lệnh nghiệm thu chuẩn cho mỗi đầu nối (bước 6)

Cả hai là runner nằm trong repo mã hiện hữu, không thêm tài liệu, và trả một dòng ACCEPTANCE: PASS|FAIL bao trùm cả hồi quy lẫn kiểm capability/schema:

  • Claude: cd /opt/incomex/claude-mcp && python3 tests/run_acceptance.py --expect-fingerprint fca7e350ffd2 (hồi quy: mỗi module một tiến trình; cổng phát hành: đọc bề mặt từ container đang chạy incomex-claude-mcp — BUILD_ID, số tool, vân tay, và tham số bắt buộc của từng năng lực.)
  • GPT: python3 /opt/incomex/docker/agent-data-repo/tests/run_acceptance.py --expect-schema-hash f6a27f2622ff (hồi quy: tests/continuation/ trong container dùng-một-lần dựng từ image production, nguồn mount :ro, kèm workspace-tools.json production cho các ca K10; cổng phát hành: tools/list qua route công khai. Secret của route đọc từ cấu hình nginx lúc chạy và không bao giờ in ra.)

Cả hai có --no-live để chạy riêng phần hồi quy; khi đó lệnh trả INCOMPLETE, không bao giờ PASS.

13.10.7 Hồi quy toàn bộ + kiểm máy chủ sống (bước 7)

Chạy bằng chính hai lệnh chuẩn trên:

Đầu nối Hồi quy Cổng phát hành Kết
Claude 39/39 (5 module, mỗi module một tiến trình) build claude-mcp-harden-20260919.1 · 23 tool · vân tay fca7e350ffd2 · 9/9 năng lực 🟢 ACCEPTANCE: PASS
GPT 72/72 trong 324,6 s (tests/continuation/) 1.3.0 · …20260919.1+b1gdc+workspace-20260918 · hash f6a27f2622ff · 37 tool · 9/9 năng lực 🟢 ACCEPTANCE: PASS

Trong 72 ca có ca nợ §13.6b test_background_operation_survives_initial_call — nay xanh, và lần này xanh có trọng lượng vì nó chờ theo tín hiệu chứ không theo đồng hồ (13.10.3).

Không phát hiện lỗi runtime ⇒ KHÔNG restart gì (đúng §7 của work order). Kiểm sau khi chạy xong: incomex-claude-mcp StartedAt=2026-09-18T23:13:54Z · incomex-agent-data StartedAt=2026-09-18T23:16:38Z, cả hai RestartCount=0, healthy, 48 image không thêm không bớt, 0 Traceback ở cả hai trong 30 phút cuối, và không còn container thử nào sót. Mọi lượt chạy test dùng container dùng-một-lần docker run --rm với nguồn mount :ro.

Tự kiểm PL1: /opt/incomex/mcp-roots/gh/.git vẫn duy nhất incomex-mcp-helper:incomex-mcp-helper và git-của-root vẫn bị từ chối "dubious ownership" (Agent không thêm safe.directory); không fetch/pull/checkout/commit ở cả hai clone làm việc. HEAD kho chung vẫn c4d7f00 — Agent không đẩy gì.

13.10.8 Bề mặt CHÍNH XÁC mà K1–K9 phải thấy (tầng CLIENT/CROSS — việc của hai chat)

Agent không chạm K1–K9. Ghi sẵn con số để hai chat đối chiếu thay vì đoán:

Mã Ai Kỳ vọng CHÍNH XÁC
K1 Claude Chat chat mới sau khi ngắt/kết nối lại: đúng 23 tool; vps_status(muc='readiness') in claude-mcp-harden-20260919.1 · 23 tool · vân tay fca7e350ffd2. Lệch số hoặc lệch vân tay = client còn giữ danh sách cũ
K2 GPT Chat chat mới sau Refresh: đúng 37 tool; gọi thẳng workspace_edit, workspace_transaction, workspace_exec, workspace_task_start có operation_id mà client không chặn. Máy chủ đã đủ schema (13.10.5) ⇒ còn chặn = client chưa refresh
K3 Claude → GPT GPT gọi workspace_stat/workspace_log trước khi đọc gì khác vẫn thấy commit của Claude; đáp phải có source_head = commit đó và refreshed_at khác null
K4 GPT → Claude fs_log/fs_diff lạnh thấy commit của GPT; đáp phải có dòng NGUỒN: đã pull ngay trước khi đọc · source_head=<commit đó>
K5 Claude cũ → GPT sửa VERSION_CONFLICT, tệp giữ nguyên v2 của GPT
K6 chéo hai chiều ghi thành công, commit cha là commit của bên kia, tệp của bên kia còn nguyên
K7 mỗi bên cùng operation_id + payload y nguyên ⇒ đúng 1 commit; lần hai REPLAY/replayed:true, không commit mới
K8 mỗi bên id của copy đem dùng cho move ⇒ OPERATION_ID_REUSED nêu tên cả hai tool, tệp nguồn không bị động
K9 GPT Chat hai dấu cách cuối dòng ⇒ THÀNH CÔNG kèm warnings, byte giữ nguyên (chặn chỉ dành cho dấu hiệu xung đột)

Tệp thử chỉ nằm trong _thu-nghiem/client-20260919/ (P08).

13.10.9 Nợ còn lại sau lượt này

  • Phía đầu nối: không còn nợ nào đã biết mà chưa xử lý. Nợ §13.6b đã trả (13.10.3 + 13.10.7); nợ "đỏ giả do env cấp module" phía Claude (§13.9.4) đã trả bằng runner, không đụng runtime.
  • Chỉ còn: hai client làm mới + K1–K9. Không việc nào của Agent chặn hai việc đó.
  • Hẹn đo lại sau 2026-09-25 (giữ nguyên từ §12.16): GC của staging upload / result tạm.
  • Chưa làm, cố ý (ngoài phạm vi, chờ Owner/Codex): trần client_max_body_size cho route GPT (đề xuất 64m, §12.18); cron git add -A + force-push (13.10.2); hai dịch vụ FAILED lúc khởi động (§12.17); 171 tệp bẩn có sẵn trong /opt/incomex.
  • Agent không chạm AGENTS.md, README.md, COLLAB.md, PROMPT.md; không xoá dữ liệu/nhánh/image; không đổi auth/URL/secret; không push.

13.11 Đợt vá vòng đời tệp (G1–G16 / C1–C16) — báo cáo Agent 15:20 (+07) ngày 2026-09-19

Nguồn: phiên Claude Code chạy addendum G1–G16/C1–C16. Agent chỉ trả báo cáo trong chat; Owner chuyển cho Claude Chat; Claude Chat chép nguyên văn vào đây lúc 08:45Z (không sửa chữ). Trạng thái: GPT-side đã code + test, CHƯA deploy; 3 quyết định phạm vi chờ Founders (COLLAB P12: D09 NFC, D10 không mở HTTP mới, D11 gh text-only). Đính chính của Claude Chat (COLLAB P12): dòng "MOVE DIR · Claude · unverified" là sai — đã PASS thật ở commit f5870e9; dời thư mục vào chính con → invalid_move.

46 passed, 0 failed across both continuation suites. Here is where things stand — I'm stopping before the remaining items because three of them conflict with the standing contract or need your decision.

Done and tested — GPT connector (agent-data), backend only

ID: G1
Change: Every create/destination path now makes its missing parents automatically — write_new,
import_file, upload commit, copy, move, and transaction create/copy/move. No flag, matching
what the Claude connector already did. New dirs inherit the parent's mode (setgid kept) so
the shared ui root stays writable by both sides.
Test: ✅
────────────────────────────────────────
ID: G2
Change: workspace_stat gives a directory a deterministic tree token; copy/move expand a
directory into its files inside one commit, guarded by that token.
Test: ✅
────────────────────────────────────────
ID: G3
Change: workspace_import_file gets a derived request identity (the volatile download_url
excluded, file_id kept), so a retry after a lost response replays instead of writing twice.
A failed download is now a clean refusal, not a quarantined id.
Test: ✅
────────────────────────────────────────
ID: G4
Change: workspace_upload_begin dedupe moved ahead of the FILE_EXISTS check and extended to
committed uploads, so a lost answer returns the original upload and its result.
Test: ✅
────────────────────────────────────────
ID: G7
Change: Directory copy/move into its own descendant (or ancestor) is refused before anything
is read. Defect in the G2 code I had just written.
Test: ✅
────────────────────────────────────────
ID: G11/G15
Change: Rollback removes the parents it created; a move removes the folders it emptied. Same
defect class.
Test: ✅

SCHEMA_CHANGED: NO — no property, required field, or tool added or removed. Tool count stays 37. Only five tool descriptions changed (they previously stated "Existing parent directories required", which is now false). Old clients keep working without refreshing; a refresh only buys accurate descriptions.

Not deployed. Production still runs agent-data-hardening:20260919-harden, untouched; rollback tag agent-data-hardening:rollback-pre-mkdir-20260919 created. I held the build deliberately — you asked for exactly one client refresh, and the open decisions below can still change the schema.

Verified on the Claude side (real calls, not code reading)

- C15 ghost folders — confirmed. /opt/incomex/mcp-roots/gh/_thu-nghiem/audit-claude-20260919/a/b/c is empty and untracked; fs_list shows it, GitHub does not.
- C2/C8 — confirmed. fs_stat on a directory returns no version at all, and fs_copy refuses directories outright ("thư mục: chuyển từng file").
- C16 — confirmed in code (fsroots.py:1288): files ≤ 51200 B are fully replaceable with no expected_version. This one is not a contract change — README §3 already requires a version on every overwrite, so fixing it enforces the contract.

Three items I will not do without your decision

1. C14/G14 (NFC normalisation) contradicts the governing contract. README §8 v1.3 says, verbatim: "Giữ nguyên path/tên file Unicode kể cả dạng NFD: không tự normalize/rename." C14 asks for the opposite at every entry point. That is a D decision for the Founders, not something an Agent edits underneath the contract.
2. C13/G13 (signed one-time export GET) is a new public HTTP surface on a public-facing host — nginx route, token store, TTL. That is outside "sửa connector" and squarely inside this RUN's own stop rule.
3. G9/C9 (binary byte-for-byte) cannot be satisfied as specified. Both connectors are deliberately text-only on the Git root: GPT's upload commit does raw.decode('utf-8'), Claude's fs_copy refuses NUL bytes on gh. Binary support is a design change, not a fix.

G12/C12 I cannot complete at all: it requires the two real chat clients. I can drive the Claude connector from here (I did, above), but I cannot drive the ChatGPT client.

Lifecycle matrix

| Stage | GPT | Claude | Status |
| DISCOVER · READ · SEARCH · STAT(file) · EDIT · COPY FILE · CONCURRENCY | ✅ | ✅ | PASS (pre-existing + suite) |
| STAT (directory version) | ✅ new | ❌ none | Claude GAP |
| CREATE nested · MOVE FILE nested | ✅ new | ✅ existing | PASS |
| IMPORT | ✅ + derived idempotency | N/A | PASS |
| EXPORT | ❌ | ❌ | BLOCKED (C13) |
| LARGE UPLOAD | ✅ resumable | fs_write ≤20 MB | partial |
| BINARY | ❌ | ❌ | GAP — design conflict |
| COPY DIR | ✅ new | ❌ refuses | Claude GAP |
| MOVE DIR | ✅ new | unverified | Claude GAP |
| TRANSACTION (incl. trees) | ✅ | files only | Claude partial |
| IDEMPOTENCY | ✅ 5 + import | ✅ | PASS |
| FAILURE/RECOVERY (no ghosts) | ✅ new | ❌ C15 proven | Claude GAP |
| CLIENT BINDING | not deployed | not deployed | PENDING |

Repo cleanup (Việc 2) not started — your own gate is "sau khi toàn bộ matrix xanh", and the matrix cannot go green until the three decisions above are made.

What I need from you: (a) NFC — keep README §8 or supersede it with a new D? (b) Export endpoint — approve the new HTTP surface, or drop C13? (c) Binary on the Git root — lift the text-only rule, or accept ui-root-only? With those, I can finish the Claude-side C1–C16 work and deploy both connectors in a single refresh.

Background check confirms the count I reported: the live container's workspace module registers exactly 26 tools (the same list my test asserts), and server.py adds the 11 knowledge-base tools — 37 total, unchanged, with no delete/chmod/symlink/mkdir tool among them.

13.11.1 RUN R03-FILE-LIFECYCLE-20260919-01 — tầng máy (Claude Code, 2026-09-20)

Lệnh RUN của Owner, PROMPT READY@7d0521916bbc9705eade326b3dcbb00c30f81c53 (đã so đủ 40 ký tự trên bản sao riêng /tmp/r03-20260919/repo; commit cuối chạm PROMPT.md đúng bằng SHA đó, hai Founder đã REVIEWED@ cùng SHA). Áp PL1–PL3 theo đính chính 7159559: không fetch/pull/checkout/commit trong /opt/incomex/mcp-roots/gh hay /opt/incomex/data/workspace-tools/github-workspace bằng tay — mọi thay đổi ở hai clone đó chỉ đi qua chính hai đầu nối; repo mã VPS commit theo path; không xoá dữ liệu/nhánh/image; không mở route/nginx mới.

Commit mã (VPS là SSOT của mã): /opt/incomex → 5825714 [Claude Code] R03 · claude-mcp + trợ lý · vòng đời tệp (12 tệp). /opt/incomex/docker/agent-data-repo → effb04e [Claude Code] R03 · agent-data · vòng đời tệp (8 tệp). Không tệp nào ngoài thao tác bị cuốn vào hai commit (đã kiểm git show --stat).

A. Ma trận G1–G16 / C1–C16

ID Việc Kết Bằng chứng
G1 mọi đường tạo tự sinh thư mục cha 🟢 có sẵn từ đợt 19/09, nay ĐÃ DEPLOY; kiểm sống: workspace_write_new tạo _thu-nghiem/R03/cheo/gpt-viet.txt khi cheo/ chưa tồn tại (commit 4a0779f)
G2 mã cây cho thư mục + copy/move cả cây trong 1 commit 🟢 tests/continuation/test_workspace_folders.py (24 ca)
G3 workspace_import_file có danh tính yêu cầu dẫn xuất 🟢 cùng bộ ca trên
G4 upload_begin khử trùng trước FILE_EXISTS 🟢 cùng bộ ca trên
G5 guarded replace qua đường upload/import 🟢 KHÔNG cần đổi schema — expected_version đã có sẵn ở workspace_upload_begin/workspace_import_file; commit biến nó thành một edit có khoá. 2 ca mới: thay cả tệp dưới version đúng, và version cũ ⇒ VERSION_CONFLICT, tệp giữ nguyên
G6 tải lên dở dang sống sót qua khởi động lại 🟢 ca mới: append 2000/5000 byte → đọc workspace_upload_status từ tiến trình khác → offset=2000, status=uploading → append tiếp → commit đủ 5000 byte. Không restart production
G7 copy/move thư mục vào chính nó ⇒ từ chối trước khi đọc 🟢 test_workspace_folders.py; phía Claude kiểm sống (dưới)
G8 mã cây xác định (đi cùng G2) 🟢 workspace_stat trả version=<head>:<sha>; trùng từng ký tự hex với mã cây của đầu nối Claude (xem #5)
G9 binary byte-for-byte trên gốc gh ⬜ N/A(D11) gốc gh là text-only; phần còn lại thu về “SHA binary giữ nguyên trên ui”
G10 quét bí mật trên đường copy 🟢 không có bề mặt mới; safe_content chạy trên mọi nội dung vào Git
G11/G15 hoàn tác gỡ thư mục cha tự sinh; move dọn thư mục vừa làm rỗng 🟢 prune_empty trong workspace_operations + ca kiểm
G12 client binding 🟡 mục 4 xem mục C dưới đây — Agent KHÔNG chấm được
G13 export GET ký một lần ⬜ N/A(D10) không mở bề mặt HTTP mới trong R03
G14 normalize NFC ⬜ thay bằng D09 không normalize; chặn sinh đôi (xem #24)
G16 — ⬜ không tồn tại C16 chỉ có ở phía Claude; giữ hàng để không ai tưởng bị bỏ sót
C1 upload trực tiếp từ sandbox Claude ⬜ N/A(D10)
C2 binary phía Claude trên gh ⬜ N/A(D11) (trong báo cáo 15:20 mã C2 còn được dùng lẫn cho “fs_stat thư mục không có version” — phần đó đóng ở C4/C8, nên không còn khoảng trống dù hiểu theo nghĩa nào)
C3 fs_copy nhận CẢ thư mục 🟢 đệ quy · MỘT commit · được-ăn-cả-ngã-về-không · symlink/gitlink ở bất kỳ cấp ⇒ từ chối cả lượt · quét bí mật TỪNG tệp · trần 200 tệp / 20 MB. Kiểm sống: _thu-nghiem/R03/cay → cay-chep, 3 tệp/2 thư mục, commit e81a90c
C4 khoá theo mã cây cho copy/move/giao dịch 🟢 kiểm sống: mã cây cũ sau khi một tệp con đổi ⇒ version_mismatch (không chép gì)
C5 = C16 🟢 xem C16
C6 — ⬜ N/A(D10)
C7 copy/move thư mục vào con/cha của nó 🟢 kiểm sống: DENIED:invalid_copy “chưa có gì bị đọc hay ghi”
C8 mã cây xác định, đổi khi tệp con đổi 🟢 fs_stat(dir) trả tree_token = version; 5 ca kiểm (đổi tệp rất sâu, thêm tệp, gọi hai lần, symlink ⇒ unsafe_tree)
C9 SHA binary giữ nguyên trên ui ⬜ N/A(D11) cho gh
C10 quét bí mật từng tệp khi chép cây 🟢 ca kiểm: một tệp con chứa api_key = … ⇒ secret_in_content, KHÔNG tạo gì ở đích
C11 dọn thư mục cha tự sinh khi hoàn tác 🟢 ca kiểm: os.rename hỏng giữa giao dịch ⇒ moi/rat/sau/ biến mất
C12 client binding 🟡 mục 4
C13 export GET ⬜ N/A(D10)
C14 normalize NFC ⬜ thay bằng D09
C15 thư mục ma 🟢 trợ lý host gỡ MỌI thư mục rỗng trong bản clone ở mỗi lần pull/commit/hoàn tác. _thu-nghiem/audit-claude-20260919/a/b/c đã biến mất nhờ chính cơ chế đó, không dọn tay (kiểm bằng fs_list(depth=0) sau khi deploy)
C16 fs_write thay tệp đã tồn tại luôn cần expected_version 🟢 kiểm sống: tệp 4 byte, không version ⇒ DENIED:expected_version_required kèm cách lấy version; có version đúng ⇒ ghi được (commit bd904f3)

B. Cổng 29 năng lực

Cách chấm: một ô chỉ PASS khi PASS trên mọi root/client áp dụng. Git SSOT = GPT workspace, Claude gh; ui mutation = N/A(D08/R04); docs chỉ đọc.

# Năng lực GPT Claude Bằng chứng Kết
1 LIST/ROOT DISCOVERY workspace_list không root → danh sách gốc + writable + mode fs_list root sai → “Các gốc đang khai: gh (rw), ui (rw), docs (ro), code (ro)”; vps_status(readiness) in đủ 4 gốc + mode + read/write gọi sống cả hai PASS
2 READ/TEXT FIDELITY read cắt lát chuỗi thô, BOM/CRLF/khoảng trắng nguyên vẹn sửa trong R03: fs_read/fs_search chỉ tách ở \n (trước đây splitlines() nuốt \r của CRLF và tách cả ở \v,\f,U+2028); header nói rõ xuong_dong= và bom= ca kiểm hai phía + gọi sống (bom=có, khoảng trắng cuối dòng giữ nguyên) PASS
3 SEARCH (nội dung + TÊN/đường dẫn) workspace_search + include/exclude glob fs_search + include/exclude glob không đổi schema: tìm theo tên = glob ở include với pattern='(?s).', regex=true. Gọi sống: include=['**/that-sau/*.txt'] → đúng 2 tệp PASS
4 STAT FILE workspace_stat fs_stat bytes/sha256/version/head/freshness — gọi sống PASS
5 STAT DIRECTORY (mã cây) 🟢 từ đợt 19/09 mới: fs_stat(dir) → tree_token = version hai đầu nối ra CÙNG một chuỗi: c5359d1984902869f81575d0ab71ca6e9cbee67efa99e63fd1522f4a90bf7d4c cho _thu-nghiem/R03/cay PASS
6 LOG (lọc path + follow khi dời) mới: --follow tự bật khi path là MỘT tệp fs_log(follow=true) gọi sống: lịch sử của …/luu-tru/cay-chep/sau/that-sau/ba.txt xuyên qua cả lượt move thư mục lẫn lượt copy PASS
7 DIFF giữa HAI phiên bản bất kỳ mới: workspace_diff(from_ref[, to_ref]) mới: fs_diff(from_version[, to_version]) → hành động trợ lý git_diff_range ca kiểm hai phía (giới hạn theo path, mặc định to=HEAD, ref không phải hex ⇒ từ chối); cổng phát hành đọc thấy tham số trên tools/list sống PASS (client: xem mục C)
8 CREATE nested 🟢 🟢 giao dịch tạo cay/sau/that-sau/ba.txt từ con số không, 1 commit 5a197f9 PASS
9 EDIT + literal replace_all có đếm mới replace_all/expected_count mới replace_all/expected_count gọi sống phía Claude: expected_count=2 ⇒ edit_count_mismatch (không ghi gì), expected_count=3 ⇒ “chỗ đã thay=3”, commit 324d40b PASS
10 GUARDED REPLACE 🟢 (workspace_edit bắt buộc version; upload/import có expected_version) mới (C16): mọi ghi đè đều cần version gọi sống + ca kiểm PASS
11 COPY FILE 🟢 🟢 gọi sống: chép vào thư mục chưa tồn tại, sha256 khớp, commit 087691c PASS
12 COPY DIRECTORY 🟢 mới (C3) 1 commit cho cả cây; symlink/gitlink ⇒ từ chối; quét bí mật từng tệp; trần tệp/byte PASS
13 MOVE FILE 🟢 🟢 gọi sống a3f5702 PASS
14 MOVE DIRECTORY (+prune) 🟢 mới: fs_move nhận expected_version (mã cây); dọn thư mục vừa làm rỗng gọi sống: cay-chep → luu-tru/cay-chep (c325084), không còn thư mục ma; move tệp cuối ⇒ “thư mục rỗng đã dọn: _thu-nghiem/R03/chi-mot” PASS
15 TRANSACTION FILE+TREE 🟢 mới: op copy nhận thư mục; chỉ stage đúng tệp của thao tác ca kiểm: 1 op copy thư mục + 1 op write ⇒ MỘT commit, đúng 4 path PASS
16 DELETE — — luật nền của chủ: hai đầu nối cố ý không có tool xoá N/A (quyền Owner)
17 IMPORT ATTACHMENT 🟢 backend (G3, fixture HTML 2–3 MB) ⬜ Claude direct import = N/A(D10); phần client của GPT (gọi thật từ một attachment) thuộc mục 4 PASS backend · client → mục 4
18 LARGE/RESUMABLE UPLOAD 🟢 (G4 + G6) ⬜ Claude direct upload = N/A(D10) PASS
19 EXPORT/DOWNLOAD/BACKUP — — N/A(D10)
20 CONCURRENCY/LOCK expected_version/expected_head; mã cây cho thư mục mới: mã cây làm khoá cho copy/move/giao dịch gọi sống: mã cây cũ sau khi tệp con đổi ⇒ version_mismatch; BUSY trả retry_after, không mutation nửa vời PASS
21 IDEMPOTENCY/LOST RESPONSE operation_id 7/7 operation_id 5/5 gọi sống: cùng id + cùng payload ⇒ [REPLAY …], không commit thứ hai PASS
22 FAILURE/RECOVERY HYGIENE 🟢 mới: hoàn tác gỡ cả thư mục cha tự sinh; trợ lý prune sau mỗi restore ca kiểm hai phía; clone sạch sau mọi ca từ chối PASS
23 DANGEROUS PATHS 🟢 🟢 gọi sống: ../../../etc/passwd ⇒ path_not_allowed; .git, symlink, tên khớp danh sách chặn ⇒ từ chối PASS
24 UNICODE/NFC/PORTABILITY mới NAME_COLLISION mới name_collision_nfc / name_collision_case Chính sách rõ cho hoa-thường: TỪ CHỐI (Linux giữ được cả hai, macOS/Windows thì không). Gọi sống hai phía: Bao-Cao.md có sẵn ⇒ bao-cao.md bị từ chối và nêu tên tệp đang có. Nhánh NFC/NFD chứng minh ở tầng đầu nối (ca kiểm): tầng chat tự gộp NFC nên không gõ được tên NFD từ client PASS
25 POLICY/GIT SAFETY 🟢 🟢 text không phải UTF-8 ⇒ từ chối rõ (UTF8_TEXT_REQUIRED / binary_file); chỉ stage tệp của thao tác; main chỉ fast-forward, không force; D12 giữ nguyên (Agent không đổi quyền nào của Owner) PASS
26 FRESHNESS/CROSS-CLIENT 🟢 🟢 gọi sống HAI CHIỀU: GPT ghi 4a0779f ⇒ Claude đọc lạnh thấy ngay; Claude ghi e574fd4 ⇒ GPT workspace_stat thấy ngay với refreshed_at PASS
27 CLIENT BINDING CONTRACT ⏳ ⏳ CHƯA CHẤM — bắt buộc, không được ghi N/A. Xem mục C mục 4
28 HISTORICAL READ / SAFE RESTORE mới: workspace_read/list(ref) + op restore mới: fs_read/fs_list(ref) + op restore gọi sống trên chính repo thật: hoàn tác 324d40b ⇒ ba chỗ “Ban chạy tốt” trở lại “Ban chạy” mà dòng thêm sau đó vẫn còn (commit bù 0cf7762); hoàn tác một commit ĐÃ TẠO tệp: không có archive_dir ⇒ delete_forbidden, có ⇒ tệp được DỜI vào vùng lưu trữ trong cùng commit a6d617f. Ca kiểm: hunk chồng lấn ⇒ DỪNG cả giao dịch, HEAD và cây làm việc y nguyên PASS
29 OWNER-VIEW PUBLISH BOUNDARY — — connector không có copy chéo workspace/gh → ui N/A(R04/D08)

28/29 dòng PASS hoặc N/A có số quyết định. Dòng còn lại là #27, thứ về bản chất chỉ đo được SAU khi deploy (DANH-MUC §2 đã ghi “cần nghiệm thu sau deploy”), nên cổng §3 được đọc là: không còn FAIL nào ở 28 dòng đo được trước deploy ⇒ deploy; #27 thuộc mục 4 và Agent không được tự chấm.

C. Deploy (một lần cho cả hai) và hợp đồng client

Test trước deploy: Claude 115/115 xanh (7 module, mỗi module một tiến trình; 76 ca mới), GPT 124/124 xanh (28 ca mới). Hai ca kiểm cũ bị sửa vì kiểm sai thứ, không phải vì hạ chuẩn: (a) fixture kho bare của GPT không ghim symbolic-ref HEAD trong khi máy chủ không đặt init.defaultBranch ⇒ mọi ls-tree HEAD chết với mã 128 (chính là 7 ca đỏ khi bắt đầu lượt này); (b) hai ca H12 ghim nguyên chuỗi ngày 20260919 trong BUILD_ID/CONNECTOR_SCHEMA_VERSION ⇒ đỏ ở MỌI đợt sau, nay kiểm hình dạng <tên>-<YYYYMMDD>.<n> và bắt buộc phải đổi.

Claude — Incomex VPS GPT — Incomex AgentData
image cũ (tag lùi) claude-mcp-local:rollback-pre-r03-20260920 agent-data-hardening:rollback-pre-r03-20260920
image mới claude-mcp-local:r03-20260920 agent-data-r03:20260920-lifecycle
bản dựng công bố claude-mcp-r03-lifecycle-20260920.1 agent-data-r03-lifecycle-20260920.1+b1gdc+workspace-20260918
vân tay / hash fca7e350ffd2 → 4f1000e9aad3 f6a27f2622ff → dbbfc590a969
số tool 23 → 23 (không đổi) 37 → 37 (không đổi)
run_acceptance.py sau deploy ACCEPTANCE: PASS (115 ca + cổng phát hành) ACCEPTANCE: PASS (124 ca + tools/list qua route công khai)

Trợ lý host incomex-mcp-helper.service và incomex-workspace-exec.service được restart (điều kiện PL2 đã kiểm trước: 0 manifest prepared/push_unknown/rollback_conflict, hàng đợi việc rỗng).

🔴 Một số đo lệch chuẩn, nói thẳng: incomex-claude-mcp healthy sau 96 s; incomex-agent-data healthy sau 124 s — tức quá 4 giây so với mốc “healthy trong 2 phút” của PROMPT §3. Đây là nhịp khởi động sẵn có của image đó (start_period 45 s + interval 15 s ⇒ lần thăm dò thành công đầu tiên rơi vào mốc 124 s), không phải triệu chứng của đợt sửa này; hai container từ đó tới giờ healthy, RestartCount=0. Agent KHÔNG hoàn nguyên, và ghi rõ con số ở đây để Host quyết có coi là đạt hay không.

Cờ hợp đồng client (§2 #27):

  • TOOL_LIST_CHANGED = NO — không thêm/bớt/đổi tên tool nào (23 và 37 giữ nguyên).
  • TOOL_INPUT_SCHEMA_CHANGED = YES — đúng MỘT đợt, ở cuối: Claude fs_diff (version thành tuỳ chọn + from_version/to_version), fs_read (+ref), fs_list (+ref), fs_move (+expected_version); GPT workspace_diff (+from_ref/to_ref), workspace_read (+ref), workspace_list (+ref), workspace_edit và op edit của workspace_transaction (+replace_all/expected_count), workspace_transaction (+ biến thể op restore). Các thay đổi nằm TRONG edits/ops của đầu nối Claude không tính là đổi schema vì hai tham số đó vốn là mảng đối tượng tự do.
  • TOOL_METADATA_CHANGED = YES — mô tả của cả hai bên đổi (đó là lý do vân tay đổi).
  • CLIENT_REFRESH_REQUIRED = YES — làm mới/kết nối lại đúng một lần cuối, rồi mở chat mới.

D. Smoke mà Agent thực sự làm được — và chỗ Agent dừng

Agent chạy nhóm ca ghi thật trên Git SSOT root của cả hai đầu nối, trong _thu-nghiem/R03/ (theo P11(c)): tạo cây lồng 1 commit · fs_stat thư mục lấy mã cây · chép cả thư mục 1 commit · chép vào chính con ⇒ từ chối trước khi đọc · ghi đè không version ⇒ từ chối · mã cây cũ ⇒ từ chối · replace_all sai expected_count ⇒ huỷ cả lượt, đúng expected_count ⇒ 3 chỗ · tên chỉ khác hoa-thường ⇒ từ chối và nêu tên có sẵn (cả hai đầu nối) · chuyển cả thư mục · chuyển tệp cuối ⇒ dọn thư mục rỗng · hoàn tác một commit giữ nguyên thay đổi sau · hoàn tác commit đã tạo tệp ⇒ no-delete · operation_id phát lại · chéo GPT↔Claude hai chiều · .. bị chặn.

🔴 Chỗ Agent DỪNG, đúng theo §4/§5: phiên client đang chạy này giữ danh sách tool trước deploy (kiểm được: vps_status(readiness) của máy chủ in 4f1000e9aad3, trong khi schema mà client giữ vẫn là bản cũ — fs_diff còn bắt buộc version, fs_read chưa có ref, fs_move chưa có expected_version; phía GPT, mô tả workspace_move mà client giữ còn nói “Existing parent directories required”, điều đã sai từ đợt 19/09). Vì vậy #27 và các ca cần tham số MỚI (fs_diff(from_version), fs_read/fs_list(ref), fs_move(expected_version), workspace_diff(from_ref), workspace_read/list(ref), workspace_edit(replace_all), workspace_transaction(op=restore)) chưa được chấm ở tầng client — chúng đã PASS ở tầng máy chủ (ca kiểm + tools/list sống của cổng phát hành), nhưng theo §2 #27 “mỗi surface phải GỌI TOOL THẬT”. Cần đúng một lần refresh plugin GPT + ngắt/kết nối lại connector Claude, rồi nghiệm thu ở chat mới trên GPT Chat, GPT Work, Claude Code (không được N/A) và Claude Chat.

Ngoài phạm vi, không chặn, ghi để khỏi quên: DANH-MUC-CONG-CU.md §2 vẫn đang là bảng chấm TRƯỚC R03 — Agent cố ý không sửa (đó là tài liệu chấm của Host, và phải đợi tầng client xong); _thu-nghiem/R03/ còn nguyên tệp thử theo P11(c) (xoá là quyền Owner khi đóng R03); chỗ lưu trữ của op restore giữ nguyên đường dẫn gốc bên dưới archive_dir (dài dòng nhưng không mơ hồ).

R03-FILE-LIFECYCLE-20260919-01: MACHINE_DONE — client/cross còn mục 4 · KB §13.11


13.11.2 — RUN R03-NAME-TWIN-REPAIR-20260920-01 (2026-09-20): #24 ở MỌI đường tạo tên

Người làm: Claude Code. Giấy phép: PROMPT_SHA 8114352e5599b826657dcd302fd4cbdf0155049d · APPROVAL_COMMIT 75471ff5ae91a5abc569c184d52f1c7aa63ac346 · HEAD lúc khởi động 006f693d29423f474fb85f9bcb08762d421a8b03. Giữa lượt, repo được tái cấu trúc; bridge P16 (work/mcp-workspace/COLLAB.md @ 3117f3a247d41d6023683c9a1ea8ac6b8382b5ff) cho run này chạy tiếp theo gate cũ, thu phạm vi còn đúng #24 D1–D4, và chuyển mọi path thử sang work/mcp-workspace/_thu-nghiem/R03/. Agent đã đọc lại remote trước khi deploy/ghi, và áp đúng.

A. Bảng entry point × cổng — đọc từ MÃ, không từ trí nhớ

grep mọi chỗ rename / mkdir / ghi tệp mới / write-blob ở cả hai đầu nối + trợ lý host. Số dòng là bản SAU khi sửa. #24 = sinh đôi mọi thành phần path · #23 = path nguy hiểm · #10 = bắt version khi thay tệp có sẵn · #25 = chỉ text UTF-8 + quét bí mật.

Claude — claude-mcp/app/fsroots.py

Entry point #24 #23 #10 #25
fs_write (1611) FAIL → SỬA: chỉ so cái lá, và bỏ qua cả nó khi cha chưa có ⇒ _name_guard(base, real) 1665 PASS _resolve 167 (.., tuyệt đối, .git, symlink, ngoài gốc) PASS expected_version_required 1646 + overwrite_guard 1655 PASS _public_policy 1666 · NUL 1620 · UTF-8 260–264
fs_copy tệp + cây (1732) FAIL → SỬA: 1767 (đích) + 1793 (mọi tệp cây nở ra). COPY không bao giờ bỏ qua nguồn PASS _resolve + unsafe_tree 606/617 PASS dest_exists (copy không ghi đè) PASS _public_policy 1723/1774 · binary_file 1776
fs_move tệp + cây (1968) FAIL (D1) → SỬA: 1999 keep=_move_keep(base, src) — bỏ qua ĐÚNG một tên, tên của chính nguồn PASS + invalid_move (vào chính nó / vào cha) PASS version tệp, mã cây cho thư mục N/A (không sinh byte mới)
fs_transaction op write tạo mới (2239) FAIL (D4) → SỬA: trước đó KHÔNG gọi guard nào PASS _resolve PASS overwrite_guard 2242 PASS _public_policy 2277
fs_transaction op copy tệp (2274) / thư mục (2250 + mỗi tệp nở ra 2260) FAIL một phần → SỬA: trước chỉ so cái lá PASS PASS dest_exists PASS binary_file 2273
fs_transaction op move (2284) FAIL (D1) → SỬA: 2284 kèm keep PASS PASS version / mã cây N/A
fs_edit (1566) N/A — không bao giờ sinh tên PASS PASS expected_version PASS _public_policy 1595
fs_transaction op restore → trợ lý gh_revert_commit_push 🔴 FAIL, KHÔNG sửa lượt này — xem mục F.1 PASS safe_rel PASS expected_head + apply --3way PASS staged_check
trợ lý host mcp_host_helper.py KHÔNG phải entry point: gốc gh là MỘT thư mục /opt/incomex/mcp-roots/gh, mount read-only vào container MCP, và người gửi việc duy nhất cho trợ lý là chính container đó ⇒ guard chạy trên đúng byte mà trợ lý sắp commit. Ngoại lệ: gh_revert_commit_push, nơi git tự tính tên (mục F.1) PASS safe_rel 177 PASS check_expected_head 282 PASS staged_check

GPT — agent-data-repo/agent_data/*

Entry point #24 #23 #10 #25
workspace_write_new → write(create=True) (tools 899) FAIL → SỬA: name_twin nay đi qua mọi thành phần MỚI PASS relative 93 (DENY, .., tuyệt đối, \) PASS chỉ-tạo-mới, FILE_EXISTS 898 PASS safe_content 58 · UTF8_TEXT_REQUIRED 804
workspace_edit → write(create=False) N/A PASS PASS VERSION_CONFLICT PASS
workspace_copy / workspace_move → transfer() → transaction() FAIL (D1 + D3) → SỬA: load(new=True) (operations 234) nay nhận keep + seen PASS PASS expected_version bắt buộc PASS
workspace_transaction create/copy/move FAIL → SỬA: cùng chỗ 234 PASS PASS PASS
expand_directories (copy/move cả thư mục) FAIL → SỬA: tên thư mục ĐÍCH nay được kiểm ở operations 65, TRƯỚC khi nở ra từng tệp PASS G7 (không lồng vào chính nó) PASS mã cây VERSION_CONFLICT PASS từng tệp
workspace_import_file → begin + commit → write(create=True) FAIL → SỬA (thừa hưởng bản vá của write) PASS PASS PASS UTF8_TEXT_REQUIRED 117
workspace_upload_begin/append/commit → write(create=True) FAIL → SỬA ở commit. begin vẫn chỉ hỏi tệp đích có chưa — xem F.2 PASS PASS expected_version 64–68 PASS 117
restore_transaction (operations 69) 🔴 FAIL, KHÔNG sửa lượt này — mục F.1 PASS relative PASS expected_head PASS staged_check

B. Luật đã cài — một hàm, hai phía như nhau

Claude _name_guard / _twin_of / _move_keep (fsroots.py) và GPT name_twin(spec, parts, keep, seen) (workspace_tools.py 685) làm đúng một việc: đi từ gốc xuống, và ở MỖI thành phần chưa tồn tại thì so nó với toàn bộ anh em trong thư mục đó. keep = những đường dẫn lượt này DỌN ĐI (nguồn của MOVE) — ngoại lệ duy nhất của §2, lấy từ operations như người gọi viết nên đổi tên cả THƯ MỤC cũng được. seen = những tên chính lượt gọi này đã hẹn tạo: chưa cái nào nằm trên đĩa lúc kiểm, nên nếu không nhớ thì hai op của một giao dịch sinh ra cay/ và Cay/ mà không thấy nhau. COPY không bao giờ truyền keep. Byte của tên đang có KHÔNG bị đổi ở bất cứ nhánh nào.

C. Đỏ → xanh (test đỏ TRƯỚC, đo trên repo tạm)

Bộ ca Trước khi sửa Sau khi sửa
claude-mcp/tests/test_fsroots_name_twin_r03_20260920.py (23 ca) 18 ĐỎ (13 failures + 5 errors) 23 xanh
agent-data-repo/tests/continuation/test_workspace_name_twin_r03.py (21 ca) 17 ĐỎ 21 xanh

Đỏ được đo bằng cách mount ĐÈ bản .bak-twin-20260920 của chính các module đó vào image đang chạy, nên con số là của cùng một tệp test, không phải của một bản test khác. Phủ §3.2: (a)(b)(c) tự đổi tên ⇒ được · (d) move sang sinh đôi của tệp KHÁC ⇒ từ chối · (e) COPY ⇒ từ chối (đối chứng âm) · (f) create/copy/move/upload/op giao dịch vào Cay/x.txt khi đã có cay/, và bản NFD/NFC ⇒ từ chối · (g) op write tạo tệp mới sinh đôi ⇒ từ chối · (h) op move tự đổi hoa-thường ⇒ được · (i) cùng operation_id ⇒ REPLAY, không commit thứ hai · (j) mọi ca từ chối: HEAD, cây làm việc, trạng thái git y nguyên, không thư mục ma. Thêm: hai op của CÙNG một giao dịch không được sinh đôi lẫn nhau.

Hồi quy: Claude 138 ca / 8 module xanh · GPT 145 ca xanh (cả hai chạy lại SAU deploy).

run_acceptance.py hai phía: PASS trước deploy và PASS lại sau deploy.

D. Deploy lần hai (§4)

Claude GPT
Tag hoàn nguyên claude-mcp-local:rollback-pre-nametwin-20260920 = e33bf3c0efe3 agent-data-r03:rollback-pre-nametwin-20260920 = d3f5a6f54744
(đã docker inspect đối chiếu) trùng ĐÚNG image id đang chạy trùng ĐÚNG image id đang chạy
Image mới claude-mcp-local:r03-nametwin-20260920 = 7e25b17974bd agent-data-r03:20260920-nametwin = 519b2965db4a
Overlay FROM claude-mcp-local:r03-20260920 + COPY app/fsroots.py app/hosttools.py FROM agent-data-r03:20260920-lifecycle + COPY workspace_tools.py workspace_operations.py server.py
Healthcheck thật start_period=20s interval=15s timeout=5s retries=5 start_period=45s interval=15s timeout=10s retries=10
Healthy sau 8 s từ State.StartedAt 113 s từ State.StartedAt
RestartCount 0 0

Trần §4 là 180 s tính từ State.StartedAt: cả hai trong mốc, không phải hoàn nguyên. Không restart trợ lý host và incomex-workspace-exec.service (mã hai cái đó không đổi). Cổng PL2 đã kiểm trước khi dựng lại agent-data: 37 manifest giao dịch, 0 cái ở prepared/push_unknown/ rollback_conflict; hàng đợi jobs/tasks rỗng. docker logs --since 20m sau deploy: 0 Traceback cả hai container.

Commit mã (cục bộ trên VPS, không push — PL1): claude-mcp 9ee71c6c68db90dd12f362540d5eb22f11d9b3eb · agent-data-repo 1d9ef1dbe999e5c9dff78f14bdc0395dc391114b. Chỉ stage đúng tệp của lượt này; cây bẩn sẵn có của claude-mcp (data/phai-cu/*, docs/mcp-writes/*) KHÔNG bị cuốn vào.

E. Smoke sống (§5) — trong work/mcp-workspace/_thu-nghiem/R03/ theo P16

# Đầu nối Lời gọi Kết quả
1 Claude fs_move ban-sao.txt → Ban-Sao.txt ✅ commit 5b2ab35 — đúng ca sáng nay còn bị từ chối
2 Claude fs_write Cay/x.txt (đã có cay/) ✅ TỪ CHỐI name_collision_case, nêu tên cay
3 Claude fs_transaction op write BAN-SAO.txt ✅ TỪ CHỐI name_collision_case (D4 sống)
4 GPT workspace_write_new ban-sao-gpt.txt ✅ commit 3d56ef6 (GPT tự tạo tệp của mình)
5 GPT workspace_move → Ban-Sao-Gpt.txt ✅ commit 7da09c7
6 GPT workspace_write_new Cay/y.txt ✅ TỪ CHỐI NAME_COLLISION
7 GPT workspace_copy Ban-Sao-Gpt.txt → ban-sao-gpt.txt ✅ TỪ CHỐI NAME_COLLISION (copy không bỏ qua nguồn)

fs_list(depth=0) sau smoke: không có bản sinh đôi nào trong cả cây — cay/ một mình, Ban-Sao.txt và Ban-Sao-Gpt.txt mỗi cái một bản. Chéo: GPT workspace_list thấy ngay commit move của Claude (head=5b2ab35, refreshed_at mới).

F. Ngoài phạm vi — ghi lại, KHÔNG sửa lượt này (P16: chỉ D1–D4)

  1. 🔴 #28 restore vẫn sinh tên mà không cổng nào nhìn, cả hai phía: trợ lý act_gh_revert_commit_push (mcp_host_helper.py ~824) và GPT restore_transaction (workspace_operations.py 69). Đảo một commit rename sẽ dựng lại tên cũ (ví dụ cay/ bên cạnh một Cay/ mới sinh sau đó), và archive_dir được tạo bằng os.makedirs trần. Agent đã soạn ca đỏ cho cả hai, rồi rút ra khi P16 thu phạm vi. Hướng sửa đã khảo sát: so tên thêm/đổi (git diff --cached --name-status -M) với cây ở HEAD trước, ngoại lệ là nguồn của rename ngược.
  2. workspace_upload_begin chỉ hỏi tệp đích đã tồn tại chưa; bản sinh đôi bị bắt ở upload_commit (đã kiểm bằng ca sống). Từ chối vẫn ĐÚNG và không ghi gì — giá phải trả chỉ là số byte đã tải lên phí công. Thêm một chốt sớm ở begin là việc của lượt khác.
  3. Image agent-data đang chạy CŨ hơn chính repo của nó: cây làm việc có bản workspace_transfer.py mang bản sửa G4 ngày 19/09 mà image chưa bao giờ nhận (Dockerfile vòng trước cố ý chỉ COPY 3 tệp). Overlay lượt này cố ý không mang nó theo — đã đối chiếu diff giữa image cũ và image mới: workspace_transfer.py y hệt.
  4. 🔴 incomex-config-drift-check.service đã DRIFT TỪ TRƯỚC lượt này, không phải do lượt này: mcp-compose MISMATCH CONTENT, TOTAL=34 MATCH=33. Baseline /var/lib/incomex-config-guard-v0/baseline/mcp-compose.content ghi ngày 2026-09-18 11:57 và vẫn nói image: claude-mcp-local:phase0.9 — tức hai vòng deploy trước (harden 19/09 và lifecycle 20/09) đều đổi đúng dòng đó mà không bless lại. Journal chứng minh: STATUS=DRIFT liên tục lúc 06:26, 06:31 … trước khi Agent deploy lúc 07:04. Agent không chạm baseline — bless lại sẽ vô tình đóng dấu cả hai vòng trước. Cần một lượt riêng của Host/Owner.
  5. /var/lib/incomex-mcp-helper/queue/in còn 5 tệp .payload mồ côi từ 18/09 (không có tệp yêu cầu đi kèm). Không chặn gì; không đụng.
  6. Đính chính đặc tả D2 trong PROMPT §1 (§3.1 buộc đọc từ mã): GPT copy/move/import/upload CÓ kiểm cái LÁ của đích — tất cả đều đi qua write(create=True) hoặc load(new=True) của giao dịch. Lỗ thật trên những đường đó là D3 (thư mục cha tự sinh) và D1 (move từ chối chính nguồn).
  7. D1 không chỉ của phía Claude như PROMPT §1 ghi: workspace_move phía GPT cũng từ chối đổi hoa-thường của chính nó (ca đỏ test_d1_move_renames_a_file_to_its_own_upper_lower_spelling). Cả hai đã sửa.

G. Các dòng bảng 29 bị chạm

#24 FAIL → PASS (cả hai phía, mọi entry point trừ restore ở F.1). #8 (nested create), #11/#12 (copy tệp/thư mục), #13/#14 (move tệp/thư mục), #15 (transaction), #17 (import), #18 (upload): hành vi nghiệp vụ KHÔNG đổi, nhưng mỗi dòng nay đi qua cổng #24 đầy đủ — bằng chứng là ca đỏ→xanh tương ứng ở mục C. #28: không đổi mã; còn lỗ #24 ở nhánh restore, ghi ở F.1 — dòng này chưa được coi là sạch #24.

H. Cờ hợp đồng client (§6)

  • TOOL_LIST_CHANGED = NO — 23 (Claude) và 37 (GPT) giữ nguyên.
  • TOOL_INPUT_SCHEMA_CHANGED = NO — vân tay Claude 4f1000e9aad3 và connectorSchemaHash GPT dbbfc590a969 không đổi trước/sau deploy. Đó chính là bằng chứng máy cho dòng này.
  • TOOL_METADATA_CHANGED = YES, nhưng CHỈ ở mã bản dựng: BUILD_ID claude-mcp-r03-lifecycle-20260920.1 → claude-mcp-r03-nametwin-20260920.1 (chính hosttools.py ghi luật phải bump khi đổi hành vi tool); CONNECTOR_SCHEMA_VERSION agent-data-r03-lifecycle-20260920.1 → agent-data-r03-nametwin-20260920.1. Không mô tả hay annotation nào bị sửa — theo §3.3, Agent đã rà và không mô tả nào đang nói SAI về tên/va chạm, nên không chạm. SERVER_VERSION giữ 1.3.0.
  • CLIENT_REFRESH_REQUIRED = YES (do đợt R03) — và CHƯA làm: theo P16 + DCLIENT01, tài khoản ChatGPT Pro không có Refresh, nên GPT phải tạo MCP app mới từ đúng server hiện hữu và Scan Tools/so schema trước khi Owner connect tay, giữ app cũ làm đường lùi. Agent không tạo/connect/ reconnect client nào.

Mốc để phiên client MỚI đối chiếu: Claude claude-mcp-r03-nametwin-20260920.1 · 23 tool · vân tay 4f1000e9aad3. GPT agent-data-r03-nametwin-20260920.1+b1gdc+workspace-20260918 · 37 tool · hash dbbfc590a969.

R03-NAME-TWIN-REPAIR-20260920-01: MACHINE_DONE — chờ tạo/connect client mới một lần · KB §13.11.2


13.11.3 — RUN R03-FINAL-CLOSE-20260920-01 (2026-09-20): E1–E4, rồi ĐÓNG BĂNG backend

Người làm: Claude Code. Giấy phép: PROMPT_SHA d15eac28b2f8a843e2a4b267cee988fbf44446ba · APPROVAL_COMMIT cf2a6ef3ce700c8fdcdb9ad959c812540c66334b · HEAD kho tài liệu lúc khởi động 34a30941ecadaa2fea02e60dfbd6f25f04cb3156. Kiểm trên bản sao riêng dưới /tmp (clone công khai Huyen1974/incomex-workspace): commit cuối chạm work/mcp-workspace/PROMPT.md đúng bằng PROMPT_SHA đủ 40 ký tự; work/mcp-workspace/COLLAB.md có REVIEWED@ của GPT Founder (không soạn) + Host READY@ cùng SHA, và RUN = ACTIVE kèm commit giấy phép. Luật nền đọc bằng git show trong bản sao đó: §1 + §6 của 7d05219:PROMPT.md, §2 của 8114352:PROMPT.md. PL1–PL3 (đính chính 7159559) áp: không fetch/pull/checkout/commit bằng tay trong hai clone làm việc; repo mã VPS commit CỤC BỘ theo path, không push; chỉ restart dịch vụ trong danh sách PL2 và chỉ khi cổng PL2 mở.

Commit mã (VPS là SSOT của mã, không push): /opt/incomex → 44ff02608e0e5dfaf28cd4c81413cea82c1c228d [Claude Code] claude-mcp + trợ lý (6 tệp). /opt/incomex/docker/agent-data-repo → 4f428c1cd3af0af00fcd67803e269f4cb2f43601 [Claude Code] agent-data (6 tệp). Chỉ stage đúng tệp của lượt này; cây bẩn sẵn có (data/phai-cu/*, docs/mcp-writes/*, ba tệp .bak-twin-20260920 untracked) KHÔNG bị cuốn vào. Khác lượt trước ở một điểm cố ý: hai dòng image: của compose lần này ĐƯỢC commit — để nguyên chúng không commit chính là thói quen đẻ ra F.3 và F.4.

A. E1 (F.3) — image = mã

So BYTE từng module python THỰC CHẠY với git show HEAD:<path> và với cây làm việc.

TRƯỚC (41 dòng · 39 "giống", 2 lệch — cả hai ở phía GPT):

module image HEAD kết
agent_data/workspace_idempotency.py e66ca880 b71fdb7e 🔴 IMAGE CŨ HƠN HEAD — thiếu G3 (danh tính yêu cầu dẫn xuất cho workspace_import_file), commit 0097c44 ngày 19/09
agent_data/workspace_transfer.py c98be851 8dcfb13c 🔴 IMAGE CŨ HƠN HEAD — thiếu G4 (khử trùng TRƯỚC FILE_EXISTS, kể cả upload đã commit) và phần G3 "tải hỏng = từ chối sạch", cùng commit 19/09

Không phải chuyện lý thuyết: cùng MỘT tệp ca kiểm mới chạy trên ba nền cho ba kết quả khác nhau — mã ở HEAD ⇒ 6 đỏ; mã lấy thẳng từ image đang chạy ⇒ 7 đỏ, ca thêm là test_e3_a_good_begin_still_works_and_replays_after_commit, gãy với FILE_EXISTS. Tức dòng #18/#21 từng được chấm 🟢 trên mã mà production chưa bao giờ chạy. Đó là F.3 đo bằng số.

SAU deploy: 41/41 dòng "giống", không còn dòng "image cũ hơn HEAD" nào trong phạm vi hai đầu nối. Bảng gồm cả hai tiến trình HOST chạy thẳng từ repo (claude-mcp/helper/mcp_host_helper.py, agent-data-repo/scripts/workspace-exec-worker.py). G4 chạy thật trên production: xem mục E. Module ngoài hai đầu nối (KB…) không chạm; không module nào của hai đầu nối còn thay đổi chưa commit.

🔴 Một quyết định phải nói thẳng. Bản HEAD của workspace_transfer.py còn mang MỘT thay đổi chạm tới CLIENT: mô tả của workspace_import_file (thêm "into a new nested folder if needed" + câu về replay). _mcp_schema_hash() là sha1 trên TOÀN BỘ MCP_TOOLS kể cả description, nên bê nguyên bản HEAD vào image sẽ làm dbbfc590a969 đổi. §2 của PROMPT cấm đổi hash (đổi ⇒ DỪNG trước deploy) và cổng Scan của app MCP mới đọc đúng con số đó. Mô tả cũ không nói SAI điều gì, chỉ nói thiếu. Nên: giữ mô tả y hệt bản production đang phục vụ, và commit chính bản giữ đó để image = HEAD. Bản mô tả đầy đủ hơn là NỢ, dành cho lượt nào được phép đổi bề mặt client. Đã đo trước khi deploy trên chính image mới: 37 tool · hash dbbfc590a969 không đổi.

B. E2 (F.1) — hoàn tác (#28) không còn sinh tên sinh đôi, cả hai phía

Luật cài đúng §2 bản 8114352, nhưng đọc từ CÂY GIT chứ không từ đĩa — vì lúc kiểm thì git apply --reverse --index đã đặt trạng thái mới lên cả cây làm việc lẫn chỉ mục, nên đĩa không còn nói được tên nào là "mới". Một hàm, hai phía như nhau:

  • before = cây ở HEAD, after = cây mà commit bù sẽ để lại — cả hai đã nở thư mục cha ra thành tên riêng, vì luật là luật của TỪNG thành phần.
  • Tên bị kiểm = after − before. Ngoại lệ = before − after, những tên mà CHÍNH commit này dọn đi — đúng là "nguồn của rename ngược", nên hoàn tác một lượt tự đổi hoa-thường vẫn làm được.
  • archive_dir đi qua cùng cổng: đích lưu trữ được đưa vào after trước khi ghi gì, rồi os.makedirs(..., exist_ok=True) trần được thay bằng tạo TỪNG thành phần, mỗi thành phần mới vẫn so với anh em ngay tại chỗ (phía GPT gọi thẳng name_twin — đúng hàm mọi đường ghi khác dùng).

Chỗ sửa: trợ lý host act_gh_revert_commit_push (claude-mcp/helper/mcp_host_helper.py) và restore_transaction (agent-data-repo/agent_data/workspace_operations.py).

Ca đỏ mà lượt trước soạn rồi rút ra không được giữ lại ở đâu cả (không trên đĩa, không trong lịch sử Git của hai repo mã) — nên ca được soạn lại theo đúng đặc tả ở F.1.

Bộ ca Trước khi sửa Sau khi sửa
claude-mcp/tests/test_helper_name_twin_r03_20260920.py (9 ca, git thật + remote thật) 5 ĐỎ 9 xanh
agent-data-repo/tests/continuation/test_workspace_restore_twin_r03.py (10 ca, E2+E3) 6 ĐỎ trên mã HEAD · 7 ĐỎ trên mã của image đang chạy 10 xanh

Phủ: hoàn tác dựng lại THƯ MỤC sinh đôi ⇒ từ chối · dựng lại TỆP sinh đôi ⇒ từ chối · bản NFD/NFC ⇒ từ chối · archive_dir sinh đôi ⇒ từ chối · ngoại lệ: hoàn tác một lượt tự đổi hoa-thường ⇒ ĐƯỢC · đối chứng: hoàn tác thường ⇒ được, lưu trữ vào thư mục mới ⇒ được, tệp bị commit cũ dời ⇒ dời ngược không tạo bản thứ hai · mọi ca từ chối: HEAD, tệp trong cây và trạng thái git y nguyên, không thư mục ma.

C. E3 (F.2) — workspace_upload_begin hỏi #24 ngay ở begin

w.name_twin(spec, w.file_parts(path)) chạy ngay sau prepare_git trong khoá gốc, TRƯỚC vòng quét resume và trước khi nhận một byte nào. Tên đã có ĐÚNG BYTE không phải sinh đôi, nên resume và replay G4 vẫn tới được câu trả lời của mình.

Một ca cũ phải sửa vì hành vi đặc tả đổi, không phải vì hạ chuẩn: test_d3_upload_commit_into_a_twin_folder_is_refused_and_writes_nothing ghim cổng ở upload_commit (khi viết nó, begin chỉ hỏi tệp đích có chưa, nên phải tải hết payload rồi mới bị từ chối). Nay đổi tên thành test_d3_upload_into_a_twin_folder_is_refused_at_begin_and_writes_nothing: cùng một tính chất — tải vào Cay/ khi đã có cay/ thì không ghi gì — chỉ khác thời điểm chứng minh.

D. Deploy — lần cuối của R03

Claude — Incomex VPS GPT — Incomex AgentData
Tag hoàn nguyên claude-mcp-local:rollback-pre-finalclose-20260920 = 7e25b17974bd agent-data-r03:rollback-pre-finalclose-20260920 = 519b2965db4a
(docker inspect đối chiếu) trùng ĐÚNG image id đang chạy trùng ĐÚNG image id đang chạy
Image mới claude-mcp-local:r03-finalclose-20260920 = 6b48248fa29f agent-data-r03:20260920-finalclose = cb20894c3bf8
Overlay FROM :r03-nametwin-20260920 + COPY app/hosttools.py FROM :20260920-nametwin + COPY 5 tệp workspace_operations/transfer/idempotency/tools + server.py
Healthcheck thật start_period=20s interval=15s timeout=5s retries=5 start_period=45s interval=15s timeout=10s retries=10
Healthy sau 6 s từ State.StartedAt 113 s từ State.StartedAt
RestartCount 0 0
run_acceptance.py sau deploy ACCEPTANCE: PASS (147 ca / 9 module + cổng phát hành) ACCEPTANCE: PASS (155 ca + tools/list qua route công khai)

Trần §3 là 180 s tính từ State.StartedAt: cả hai trong mốc. Trước deploy cả hai cũng đã ACCEPTANCE: PASS (Claude 147, GPT 155), nên đỏ→xanh là của chính lượt này chứ không phải nền sẵn.

Vì sao container claude-mcp cũng được dựng lại dù E2 nằm ở trợ lý host: hosttools.py tự viết luật "bump BUILD_ID trong cùng commit với BẤT KỲ thay đổi schema HOẶC hành vi tool", mà hành vi của op restore trong fs_transaction đã đổi (nay từ chối được). Bump BUILD_ID ⇒ phải dựng lại. Vân tay không đổi vì vân tay chỉ băm tên + mô tả tool.

Trợ lý host incomex-mcp-helper.service được restart sau khi kiểm cổng PL2: hàng đợi /var/lib/incomex-mcp-helper/queue/in không có tệp yêu cầu nào (chỉ 5 .payload mồ côi từ 18/09, để nguyên), 223 manifest giao dịch = 145 committed + 73 applied + 5 rolled_back, 0 cái ở prepared/push_unknown/rollback_conflict, hàng đợi jobs/tasks rỗng. incomex-workspace-exec.service KHÔNG restart (mã của nó không đổi).

🔴 Một quan sát phải nói thẳng, không phải của lượt này: docker logs của incomex-agent-data sau deploy có 1 Traceback lúc khởi động — zipfile.BadZipFile: File is not a zip file trong langroid/parsing/search.py → nltk.data.find. Đây là một cuộc đua lúc boot: hai worker uvicorn cùng tải bộ liệu NLTK vào /root/nltk_data (nằm TRONG container, dựng lại mỗi lần recreate), một cái đọc wordnet.zip trong khi cái kia còn đang ghi. Worker chết, được sinh lại, cả hai Application startup complete, các zip hiện đều hợp lệ, container healthy, RestartCount=0, acceptance PASS. Không phải do lượt này: delta của server.py giữa image lùi và image mới chỉ đúng một dòng CONNECTOR_SCHEMA_VERSION (đã diff để chứng minh), và cả stack không có khung nào của agent_data.workspace_*. Cũng chính nó giải thích 113 s tới healthy. Ghi nợ, không sửa (ngoài 4 việc). claude-mcp: 0 Traceback; journal trợ lý: 0 lỗi.

E. Smoke sống (§4) — trong work/mcp-workspace/_thu-nghiem/R03/e2e-finalclose/

# Đầu nối Lời gọi Kết quả
1 Claude fs_write cay/a.txt → fs_move sang khac/a.txt (98d7400) → fs_write Cay/b.txt (369ad88) ✅ ba bước đều được; cay/ rỗng đã được dọn nên Cay/ là tên hợp lệ
2 Claude fs_transaction op restore version=98d7400 ✅ TỪ CHỐI name_collision, nêu đúng …/claude/Cay đang có, "KHÔNG có gì bị ghi; bản clone ĐÃ trả về trạng thái trước"
3 Claude hoàn tác một commit SỬA (2870d19) ✅ được — commit bù e301fbc, nội dung trở lại 26 B
4 GPT workspace_write_new cay/a.txt → workspace_move sang khac/a.txt (8e903ed) → workspace_write_new Cay/b.txt ✅
5 GPT workspace_transaction op restore version=8e903ed ✅ TỪ CHỐI NAME_COLLISION, "Nothing was written; the undo is rolled back"
6 GPT hoàn tác một commit SỬA (56fdca9) ✅ được — commit bù a41cc97
7 GPT workspace_upload_begin vào cay/tai-len.txt (đã có Cay/) ✅ TỪ CHỐI NAME_COLLISION ngay ở begin, không upload_id, không nhận byte nào
8 GPT upload_begin + append + commit vào tên sạch tai-len/moi.txt (49ed255) ✅
9 GPT G4: gọi LẠI đúng upload_begin đó sau khi đã commit ✅ trả về cùng upload_id, resumed:true, status:"committed" kèm nguyên result đã ghi — không FILE_EXISTS

Ca 9 chính là bằng chứng "G4 chạy thật" mà E1 đòi: cho tới sáng nay production trả FILE_EXISTS ở đúng lời gọi này. Lưu ý câu chữ: §4 của PROMPT viết "upload_begin cùng operation_id", nhưng workspace_upload_begin không có tham số operation_id (chỉ 7 tool có: workspace_edit, write_new, copy, move, transaction, exec, task_start). Danh tính của G4 là chính yêu cầu (root,path,sha256,total_bytes), và đó là thứ đã được đo.

fs_list/workspace_list sau smoke: không có bản sinh đôi nào — mỗi bên đúng một Cay/, không có cay/. Chéo hai chiều vẫn tươi: fs_list của Claude thấy ngay head=49ed255 (commit cuối của GPT), workspace_list của GPT thấy cây Claude vừa ghi.

F. E4 (F.4) — mốc kiểm cấu hình

Trước: TOTAL=34 MATCH=33 MISMATCH=1, STATUS=DRIFT, incomex-config-drift-check.service failed liên tục từ 2026-09-18. Đích lệch duy nhất là mcp-compose. (Nhãn trên chính unit còn nói nginx-default-conf lệch 49 dòng từ 27/07 — đã hết, nay MATCH; nhãn đó cũ.)

Từng dòng lệch, diff baseline ↔ cấu hình thật:

-    image: claude-mcp-local:phase0.9
+    image: claude-mcp-local:r03-finalclose-20260920

Đúng MỘT dòng, và đúng là dòng image: — baseline đóng dấu 2026-09-18 11:57, còn bốn vòng deploy sau đó (harden 19/09, lifecycle 20/09, nametwin 20/09, finalclose hôm nay) đều đổi đúng dòng đó mà không bless lại. Điều kiện Owner đặt cho E4 thoả: không có dòng lệch nào khác.

Đã làm: (1) sao lưu baseline cũ ngay cạnh nó — /var/lib/incomex-config-guard-v0/baseline/mcp-compose.{content,meta,sha256}.bak-pre-finalclose-20260920; (2) bless qua ĐÚNG cổng do chính unit chỉ định, /usr/local/sbin/incomex-config-apply-v0, với candidate = bản sống y hệt từng byte và --expected-baseline = sha của chính bản sống ⇒ RESULT=APPLIED, POST_ACTION=NONE, có dòng audit và bản lùi trong rollback/. Không đổi một byte nội dung nào.

Một chi tiết của cổng, ghi để lượt sau không mất thì giờ: validator DOCKER_COMPOSE_CONFIG chạy docker compose -f <candidate> config -q, mà tệp compose này có env_file: - .env tương đối, nên lần gọi đầu bị REJECTED_UNSAFE_CANDIDATE. Cách đúng là đặt một .env RỖNG cạnh bản nháp: tệp này có 0 chỗ ${...} (đã đếm), nên .env rỗng không thể làm đổi kết quả thay biến — chính bẫy mà validator tự nêu — và không một byte bí mật nào bị chép sang thư mục thay đổi.

Sau: TOTAL=34 MATCH=34 MISMATCH=0 MISSING=0 ERROR=0, STATUS=CLEAN, unit chạy lại thoát 0 (hết failed). F.4 đóng.

G. Các dòng bảng 29 bị chạm

  • #18 LARGE/RESUMABLE UPLOAD — begin nay từ chối tên sinh đôi TRƯỚC khi nhận byte; và G4 (khử trùng trước FILE_EXISTS, kể cả upload đã commit) lần đầu có mặt trong production. Trước lượt này dòng #18 được chấm trên mã mà image chưa từng chạy.
  • #21 IDEMPOTENCY/LOST RESPONSE — G3 (danh tính dẫn xuất cho workspace_import_file, và tải hỏng = từ chối sạch thay vì cách ly id) cũng lần đầu có mặt trong production.
  • #24 UNICODE/NFC/PORTABILITY — nay sạch ở mọi entry point: đường cuối cùng còn hở (restore) đã đóng ở cả hai phía. Bảng entry point của §13.11.2 mục A: hai ô 🔴 "FAIL, KHÔNG sửa lượt này" chuyển thành PASS.
  • #28 HISTORICAL READ / SAFE RESTORE — hành vi hoàn tác không đổi (vẫn đảo delta, vẫn giữ thay đổi sau không chồng lấn, vẫn no-delete qua archive_dir), nhưng nay đi qua cổng #24 đầy đủ. Dòng này được coi là sạch #24 — điều §13.11.2 mục G nói rõ là CHƯA.

Các dòng khác không đổi. #27 CLIENT BINDING vẫn là việc của tầng client (mục §6 của PROMPT), Agent không chấm.

H. Cờ hợp đồng client

  • TOOL_LIST_CHANGED = NO — 23 (Claude) và 37 (GPT) giữ nguyên, không tool nào được thêm/bớt/đổi tên.
  • TOOL_INPUT_SCHEMA_CHANGED = NO — đo TRƯỚC deploy trên chính image mới và đo LẠI sau deploy: vân tay Claude 4f1000e9aad3 và connectorSchemaHash GPT dbbfc590a969 KHÔNG đổi.
  • TOOL_METADATA_CHANGED = YES, nhưng CHỈ ở mã bản dựng: BUILD_ID claude-mcp-r03-nametwin-20260920.1 → claude-mcp-r03-finalclose-20260920.1; CONNECTOR_SCHEMA_VERSION agent-data-r03-nametwin-20260920.1 → agent-data-r03-finalclose-20260920.1. Không mô tả hay annotation nào bị sửa — kể cả mô tả workspace_import_file, đã cố ý giữ nguyên (mục A). SERVER_VERSION giữ 1.3.0.
  • CLIENT_REFRESH_REQUIRED = YES (do đợt R03) — và CHƯA làm: Agent không tạo/connect/ reconnect client nào.

Mốc cho phiên client MỚI đối chiếu: Claude claude-mcp-r03-finalclose-20260920.1 · 23 tool · vân tay 4f1000e9aad3. GPT agent-data-r03-finalclose-20260920.1+b1gdc+workspace-20260918 · 37 tool · hash dbbfc590a969.

I. Ngoài phạm vi — ghi lại, KHÔNG sửa

  1. NỢ mô tả workspace_import_file (mục A): bản đầy đủ hơn đã có trong lịch sử Git của workspace_transfer.py, chờ lượt được phép đổi bề mặt client.
  2. tests/run_acceptance.py phía GPT vẫn mặc định ACCEPTANCE_IMAGE=agent-data-r03:20260920-lifecycle — một image cũ hai vòng. Lượt này truyền biến môi trường đúng image mỗi lần chạy. Sửa mặc định nằm ngoài 4 việc.
  3. Cuộc đua NLTK lúc boot của agent-data (mục D) — tự lành, nhưng là lý do thật của 113 s tới healthy.
  4. 5 tệp .payload mồ côi trong /var/lib/incomex-mcp-helper/queue/in (từ 18/09): để nguyên, đúng lệnh.
  5. Ba tệp *.bak-twin-20260920 untracked trong hai repo mã: để nguyên.
  6. _thu-nghiem/ không dọn; không xoá gì (xoá là quyền Owner).
  7. Không chạm AGENTS.md, README.md, COLLAB.md (gốc và work/mcp-workspace/), PROMPT.md, DANH-MUC-CONG-CU.md. Không mở route/nginx mới, không đổi auth/URL/secret, không push kho mã.

Từ đây backend R03 ĐÓNG BĂNG: không deploy gì nữa (trừ rollback) cho tới khi nghiệm thu client xong.

R03-FINAL-CLOSE-20260920-01: MACHINE_DONE — backend đóng băng, chờ tạo/connect client một lần · KB §13.11.3