Skip to content

test(mcp): audit + repro G2 cho tool schema và governance nhất quán giữa hai transport - #471

Merged
khuepm merged 9 commits into
mainfrom
feat/b-02
Sep 13, 2026
Merged

khuepm merged 9 commits into
mainfrom
feat/b-02

Conversation

@khuepm

@khuepm khuepm commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Refs #454

Chỉ là repro. PR này không sửa implementation. Bước 1 của Handoff B-02 (audit + tái hiện). Giữ Draft; chưa xin merge, chưa xin dispatch.

Thông tin handoff

  • Base SHA: 6a20441af5dde899b976479f0ed7f8d1a9341dee
  • Head mới nhất: f900936188ef56a08f17127800df1a4e58ac8871
  • Head đã review: 7cf548c0 (vòng 1) · e6633ce4 (vòng 2) · 0585cbc3 (thêm RT) · be20124d (vòng 3) · d9dbcafa (vòng 4) · ca29c1db (vòng 5) · 86373630 (vòng 6) · 50b9810e (vòng 7) · f9009361 (reviewer fix)
  • Nhánh feat/b-02 (giữ nguyên theo quyết định coordinator)
  • Đổi 2 file, đều là test:
    • apps/cms/src/services/__tests__/g2-mcp-contract-repro.test.ts
    • packages/mcp-server/src/__tests__/g2-transport-contract-repro.test.ts
  • DoD: chưa tự audit, chờ verifier theo amendment fix(agent): G1 approval decision must execute or resume the governed action #453.

0. Trạng thái từng yêu cầu

Vòng review 2 (review tại e6633ce4):

# Finding Đã xử lý
1 [P1] Inventory 63/23/40 bỏ sót tool do registerCrud sinh ✅ §5a. Kiểm kê lại bằng listTools() thật. Bỏ lọt +35 write tool, gồm 17 tool map được đúng các domain review nêu (role, policy, flow, intent, webhook, translation, cdc) — đều là control-plane, tức đúng nhóm cần governance nhất. Số hiện hành xem §5a (161 = 70 read + 1 provider + 90 mutation; 41/49). Con số 98 write / 40 / 58 mà head be20124d dùng đã bị thay thế ở vòng 4 vì phân loại theo HTTP method, không theo hành vi.
2 [P1] Đẩy việc chuyển stdio write ra ngoài #454 là sai phạm vi ✅ §6. Rút lại. Governance hai transport giữ trong #454; chỉ tách capability resolution thành dependency blocking (privilege change, cần owner sign-off) — không phải hạng mục post-#454. Kèm thứ tự thực hiện.
3 [P2] S5 kết luận sai: createItem không có admin backstop trên HTTP ✅ Sửa. isControlPlaneSkill(createItem) = false, nên không bên nào admin-gate nó; bất đối xứng của createItem chỉ ở agent governance. Đổi sang deleteItem/deleteCollection để chứng minh bất đối xứng cả hai mặt, và thêm R11 phía CMS assert trực tiếp isControlPlaneSkill (chỗ duy nhất import được) để test không thể xanh với kết luận sai như trước.

Vòng review 1 (tại 7cf548c0):

# Finding Đã xử lý
1 [P1] Content write L0/L1 chưa kiểm ở governed path GP1GP5 chạy nhánh governed; kiểm âm audit=false → GP1/GP4/GP5 đỏ đúng
2 [P1] REST fallback bằng nhãn ungoverned Bỏ hoàn toàn; thay bằng inventory + chính sách disabled
3 [P2] R5 approval ID là fake Đổi tên + fake_not_a_db_id + liệt kê 4 thứ không chứng minh
4 [P2] S3 chỉ kiểm tên key schema MCP Client/Server thật, assert -32602 + 0 REST call

3 việc tôi tự rà thêm (không ai yêu cầu, phát hiện khi đối chiếu lại handoff bước 1):

Việc Trạng thái
Ma trận thiếu cột site/tenant mà handoff nêu tên ✅ §3. Đồng thời tôi đã tự làm mất cả bảng ma trận khi viết lại body sang tiếng Việt ở head e6633ce4 — nay khôi phục đầy đủ.
Chuỗi list-tools → call chỉ chạy thật ở stdio, phía HTTP kiểm rời ở tầng service RT1RT4 chạy chính handler POST /api/v1/mcp (§4)
Chỉ nêu 3 ví dụ lệch mapping, chưa có bảng đầy đủ 23 tool ✅ §5, đo bằng listTools() thật + args handler thật

0b. Năm quyết định — ĐÃ DUYỆT (review vòng 3 đề xuất, tôi chấp nhận toàn bộ)

Ghi lại thành quyết định chính thức của bước 1. Tôi chấp nhận cả 5 không sửa đổi, kèm ràng buộc thực thi rút ra từ đo đạc ở PR này.

# Quyết định Ràng buộc thực thi
1 Phân loại theo hành vi trước khi chốt enabled/disabled. Read đã xác minh giữ REST; mutation có mapping đầy đủ đi governed; mutation chưa hỗ trợ → disabled tường minh, không REST fallback. Tìm alias (token-set, không camelCase) trước khi kết luận "không có skill". Giữ nguyên tên tool; nêu lý do disabled ở cả discovery và call. Số 41/49 ở §5a là ứng viên, không phải danh sách cuối.
2 packages/contracts là nguồn duy nhất cho Zod + metadata effect + mapping args/defaults/results; cả registry HTTP và stdio tiêu thụ cùng nguồn. Bundle vào artifact mcp-server, chốt bằng pack/install ngoài monorepo. Dùng root export sẵn có cho đợt đầu (không tạo subpath mới). contracts là workspace devDependency + tsup noExternal; Zod vẫn là dependency runtime. Không để workspace dep chưa publish làm hỏng npm i. Không trộn phương án bundle với external.
3 Capability resolution tách work item riêng, BLOCKING #454. Governance hai transport giữ trong #454. Không đổi token thành admin/wildcard, không bỏ RBAC/admin backstop/tenant. Quyền hiệu lực vẫn bị chặn bởi token/RBAC + tenant + backstop + governance; kiểm lại ở approval/resume; giữ row/field restriction. G1 #453 là gate riêng.
4 Mở rộng/chuẩn hoá đúng trong #454, theo từng tool — không map một phần rồi ghi chú field rơi. Chưa đủ support/guard/test thì disabled. Phân biệt "thiếu schema" với "thật sự cần mở rộng service". create_policy phải giữ đủ enforceTfa/ipAllow/ipDeny/validFrom/validUntil kèm chuyển đổi datetime mà route REST đang làm ⇒ cần mở rộng AccessService.PolicyInput, không chỉ harness. create_role phải mang systemKey cùng kiểm tra cấm public-role elevation như rolesRouter. CDC giữ payload_mode. Không tự cấp API/privilege mới, không bỏ guard vốn có của REST.
5 Giữ confirm: literal(true) trong schema alias stdio tại nguồn shared contract; validate trước dispatch, có thể giữ làm metadata cho audit/approval. Không bỏ khỏi schema client đang dùng. Sau validate không cần truyền vào domain service. confirm không phải human approval, không bypass L0/L1/HITL, không biến pending thành executed. Không áp confirm bắt buộc lên các HTTP skill cũ trong đợt compatibility này.

Bốn tool tôi hỏi ban đầu, chốt theo Quyết định 4 và ghi nhận đính chính của review:

  • create_collection — mở rộng payload handler theo các field REST đã hỗ trợ; SchemaService đã nhận CollectionInput nên không mặc định phải sửa service.
  • delete_field — map field_namename và truyền force đúng FieldDeleteOptions; force không thay thế approval/admin/RBAC và phải hiển thị trong action xin duyệt.
  • update_item — sửa envelope data/status + result mapping. Đính chính: skill đã nhận status, nên phát biểu cũ "thêm status vào skill" là sai; vấn đề thật là envelope (xem R13).
  • create_relation — định nghĩa schema/typed mapping từ contract relation hiện có; as never không chứng minh cần thêm tính năng domain. Giữ đủ 13 field đã quảng bá.

1. Tự sửa một kết luận sai của chính tôi

Head đầu quy 11 lỗi timeout cho B32 (tranh tài nguyên). Sai. Nguyên nhân là Node off-spec: cùng cây code, pnpm -F @lumibase/cms test trên Node 26.0.0 cho 321 file / 2703 test / 0 fail / exit 0; trên Node 24.14.0 fail 11 ca kể cả khi chạy một mình — tức không phải tải. Đáng ra tôi phải nhận ra ngay ở lần chạy đơn lẻ đó. B32 không liên quan tới ca này.

Nhờ vậy hai commit sau đi qua hook đầy đủ, Tasks: 14 successful, 14 total, không --no-verify.

2. Chỉnh lại tiền đề baseline

Hai trong ba tiền đề ở issue body đúng. Tiền đề thứ ba nói nhẹ hơn thực tế.

Tiền đề Cây hiện tại
"coreTool() khởi tạo input/output schema rỗng" Đúng, và nặng hơn: tool-registry-service.ts:71 hardcode inputSchema: {} sau ...skill, nên nó còn xoá schema mà skill đã khai (R2).
"McpService thay bằng generic object schema" Đúng (mcp-service.ts:169-172).
"npm stdio client forward REST calls" Đúng nhưng nói nhẹ: packages/mcp-server không phải forwarder mỏng của surface MCP mà là registry độc lập 161 tool REST snake_case (đo bằng listTools(); con số "~90" ở head trước là đếm bằng grep, đã sai), có Zod schema riêng, đăng ký qua SDK MCP chính thức. Giao tên với surface HTTP MCP = rỗng.

Bất đối xứng chạy hai chiều, và điều này quyết định hình dạng bản fix:

  • HTTP MCP có governance (kill switch, autonomy L0–L4, HITL, veto, audit agent_runs/agent_tool_calls, admin backstop) và không có validation input.
  • stdiovalidation thật (Zod theo từng field) và RBAC thật (PermissionService qua route REST), và không có governance agent nào.

Không bên nào là superset của bên kia. Fix phải đưa validation vào nhánh governed và governance vào stdio, không phải chọn một bên làm chuẩn.

3. Ma trận contract

3a. Cấp transport

HTTP MCP POST /api/v1/mcp npm stdio @lumibase/mcp-server
Protocol JSON-RPC 2.0 tự viết (McpService) @modelcontextprotocol/sdk chính thức
Endpoint 1 route JSON-RPC ~90 route REST khác nhau
Nguồn tool ToolRegistryService trên CORE_SKILLS (74) + override agent_tools 24 module tay, 161 tool (registerAllTools)
Đặt tên camelCase skill (createItem) snake_case REST verb (create_item)
Schema quảng bá { type: 'object' } cho mọi tool Zod theo từng field
Validation input không SDK validate trước handler
Model authorization string membership của auth.roles vs requiredCapabilities PermissionService.canAccess tại từng route
Site / tenant siteId từ c.get('siteId') (subdomain hoặc header X-Lumi-Site); registry, policy, run, tool-call đều scope theo siteId X-Lumi-Site cố định từ env LUMIBASE_SITE_ID lúc khởi động (client.ts)
Đổi site giữa các call được — mỗi request một header không — một process = một site, phải chạy process khác
Feature flag contentOs.mcp, default OFF → 404 MCP_DISABLED (RT4) không có; REST luôn bật
Admin backstop có, cho control-plane skill (mcp.ts) theo prefix, không theo transport (S5)
Kill switch / freeze có (KillSwitchService) không
Autonomy L0–L4 có (AutonomyService.resolve) — nhưng chỉ trong nhánh dangerous, xem §4 không
HITL approval có (ai_approvals + agent_approvals) không — confirm: true chỉ là prompt client
Veto window (L3) có, cho stageable item patch không
Audit run / tool-call không (chỉ activity log REST thường)
Rate limit theo tool có (agent_tools.rateLimit) không

3b. Bốn probe handoff yêu cầu

Probe HTTP MCP npm stdio
read listItems · items:read · safe · quảng bá {type:'object'} · execute list_items · GET /items/:collection · Zod-validated · execute
content write createItem · items:write · safe, không HITL · không validation · ItemService.create create_item · POST /items/:collection · Zod-validated · REST write thường
schema write createCollection · schema:create · dangerous → HITL/autonomy create_collection · POST /collections · requireSchemaPermission('schema:create') · thực thi ngay
delete deleteCollection · dangerous + control-plane → HITL admin backstop delete_collection · DELETE /collections/:name · chỉ schema:delete · thực thi ngay

4. Repro — phân loại chứng cứ rõ trước khi trích dẫn

Nhóm Test Loại chứng cứ
Schema metadata R1, R2 registry, không chạy handler
Legacy branch R3, R4, R5 fake db + audit TẮT — không phải nhánh production
Governed branch GP1GP5 audit BẬT (nhánh routes/mcp.ts dùng) + fake db table-aware; đếm row, không lưu
Capability model R6, R7, R8 mảng capability tự dựng; không authenticate user/API key thật
Naming R9, S4 duyệt toàn bộ registry
Route thật RT1RT4 handler POST /api/v1/mcp thật + harness/registry/McpService thật; chỉ db + binding là fake
MCP client thật S1, S3, S4 Client + McpServer thật qua InMemoryTransport
REST target S2, S5, S7 gọi handler thật, khoá method + path và chỉ thế. S7 KHÔNG khoá ngữ nghĩa đọc và không chứng minh no-mutation — phán đoán read/preview đến từ đọc routes CMS (có dẫn nguồn), test chỉ khoá đầu vào của phán đoán đó
Candidate alias S8 chỉ là thuật toán tìm candidate trên chuỗi. "Không trùng token" không suy ra "không có skill tương đương"
Registry thật R14 mọi skill trong bảng mapping 41 ứng viên phải có trong CORE_SKILLS; xoá/đổi tên skill ⇒ đỏ (đã kiểm âm)
Result shape S10, S11, R15 S10 cho CMS trả payload pending_approval và chứng minh delete tool vẫn báo "deleted"; R15 chạy handler skill thật với service giả
Body envelope R12, R13, S6 route + Zod thật, ItemService là spy. R12 chỉ nói về payload trong test, không nói "mọi input"; R13 là success-shaped no-op, không phải mất dữ liệu đã lưu

4a. Phát hiện P1 — vì sao kế hoạch cũ chưa đạt acceptance

createItem được xếp safe (items:write không phải schema:* mutating, tên không bắt đầu delete). AutonomyService.resolve chỉ được gọi trong if (isDangerous) (ai-harness.ts:2288-2294); dưới đó execute() rơi thẳng xuống "Step 4: Safe skill — execute directly" (:2456).

  • GP2 autonomyCap: 0 (L0) → vẫn ghi content, 0 approval.
  • GP3 autonomyCap: 1 (L1) → vẫn ghi content, 0 approval.
  • GP4 control: cùng cap, deleteItem (dangerous) → park, 1 ai_approvals + 1 agent_approvals.

Cặp GP2/GP3 vs GP4 chứng minh gate gắn vào isDangerous, không gắn vào "là write". Nên chuyển stdio sang HTTP + thêm validation chưa đạt acceptance "L0 không ghi, L1 cần approval". Đây là gap của kế hoạch, không phải regression do hai file test tạo ra.

4b. Chuỗi list-tools → call ở cấp route (mới)

RT1RT4 chạy chính handler POST /api/v1/mcp:

  • RT1tools/list end-to-end: mọi tool advertise đúng {"type":"object"}. Đây là thứ client thật nhận được.
  • RT2 — client tuân thủ tools/list vẫn không tạo được call hợp lệ: {} thoả contract quảng bá, gửi đi thì không bị -32602.
  • RT3 — quyết định governed (pending_approval + approvalId) đi trong tool result, isError: false, không phải protocol error.
  • RT4contentOs.mcp off (mặc định) thì cả surface trả 404 MCP_DISABLED.

Hai phát hiện mới từ RT, cần vào contract:

  1. Lỗi nội bộ rò nguyên văn ra client. tools/call createItem {} trả về:

    {"status":"denied","runId":"",
     "message":"Cannot read properties of undefined (reading 'length')"}

    Hai lỗi trong một response: (a) không validate ở boundary; (b) TypeError của engine bị forward làm message cho client, thay vì "field collection là bắt buộc". RT2 pin luôn chỗ rò này để bản fix không âm thầm giữ nguyên.

  2. Có hai không gian approval ID, client nhận cái nào chưa được nói ra. execute() insert vào cả hai bảng, toToolDecision() ưu tiên agentApprovalId ?? approvalId, nên client nhận id của agent_approvals. Hai id đó do hai endpoint khác nhau quyết định: POST /api/v1/agent/approvals/:id/decide khớp agentApprovals.id, còn POST /api/v1/ai/approvals/:id/decide khớp aiApprovals.id. Vì vậy acceptance "approval ID dùng được" phải nói rõ client nhận id nàoendpoint nào nhận id đó; nếu không client có thể giữ một id trông hợp lệ rồi gọi sai route.

5. Contract sửa lại

  1. SkillDefinition.inputSchema thành bắt buộc, viết bằng Zod (SDK stdio đã dùng Zod; JSON Schema suy ra từ Zod, không ngược lại).
  2. ToolRegistryService.coreTool() thôi ghi đè schema đã khai. Override agent_tools chỉ được thu hẹp, không mở rộng; validate lúc đọc, từ chối nếu mở rộng.
  3. Write/autonomy gate độc lập isDangerous (từ §4a): mọi skill có capability :(write|update|create|delete)$ phải qua autonomy resolution dù isDangerous là false. Read không đổi hành vi.
  4. Thứ tự validate: validate trước khi tạo approvaltrước mutation domain/service, kiểm lại ở executeApproved/retry. Vì ensureRun chạy trước appendToolCall, fix phải (a) không để lại run running mồ côi, (b) nếu giữ audit cho request bị từ chối thì phân biệt rõ với business mutation.
  5. Lỗi trả về phải có cấu trúc, không forward TypeError (từ §4b.1).
  6. Nói rõ không gian approval ID client nhận + endpoint tương ứng (từ §4b.2).
  7. Mapping tường minh (phương án B) — không rename, không deprecate. Bảng đầy đủ dưới đây.
  8. Không nâng quyền token, không sửa permission REST chung.

5a. Kiểm kê registry + phân loại ngữ nghĩa (sửa lần 2 — review vòng 3, P2)

Hai lần trước tôi đều sai, theo hai kiểu khác nhau:

  1. Head 0585cbc3: đếm bằng grep literal → bỏ lọt tool registerCrud sinh động.
  2. Head be20124d: đếm đúng tên nhưng phân loại sai — coi "method khác GET" là "mutation", và coi "không khớp camelCase" là "không có skill".
v1 (grep) v2 (be20124d) v3 — đo lại + phân loại
Tổng tool ~90 161 161
Handler khác GET 63 98 98 (chỉ là số handler, không phải mutation)
├ đọc/preview qua POST 7
├ hành động provider (tốn phí) 1 (translate_text)
mutation thật 63 98 90
Read (tổng) 63 70 (63 GET + 7 POST-đọc)
Mutation map được 23 40 41 (40 theo tên + 1 alias)
Mutation chưa map 40 58 49

7 tool non-GET có ngữ nghĩa đọc/preview — không được disable chỉ vì dùng POST: check_permission, check_access_conflicts, dry_run_access_import, diff_schema, lookup_tm, query_insights, run_panel. Khoá bằng test S7.

1 alias bị bỏ lọt: cdc_subscription_replay → skill replayCdcSubscription. camelCase thuần cho cdcSubscriptionReplay nên không khớp; đối chiếu bằng token-set (chuẩn hoá rồi sort token) thì khớp.

⚠️ Sửa một suy luận không hợp lệ của head trước. Head d9dbcafa/ca29c1db viết "đây là ca duy nhất, nên các tool còn lại thật sự không có skill". Sai — và nguy hiểm, vì nó có thể biến candidate set 49 thành quyết định disabled sai. Phát biểu đúng: token-set chưa tìm được candidate nào khác bằng thuật toán tên; điều đó KHÔNG kết luận được là không có skill tương đương. Một skill có thể tương đương về ngữ nghĩa mà tên hoàn toàn khác (ví dụ apply_schema ↔ một skill schema nào đó, approve_content ↔ đường approval). Cần đối chiếu ngữ nghĩa từng tool trước khi kết luận support/disabled — đúng như Quyết định 1 yêu cầu.

Riêng cdc_subscription_replay: có candidate nhưng chưa map được ngay — lệch 2 tên key (subscription_id/occurred_after vs subscriptionId/occurredAfter), có cursor không đối ứng ở skill, và skill làm String(args['occurredAfter'] ?? '') nên thiếu giá trị thành chuỗi rỗng chứ không phải "không truyền" (test S11).

Sửa các số nhỏ (test S9):

  • create_collection advertise 18 property (không phải 19) ⇒ skill đọc name + singleton nên thiếu 16 (không phải 17).
  • 32 tool trên toàn registryconfirm. Head trước ghi "~20 tool" mà không nêu mẫu đếm — nay ghi rõ mẫu.
  • Danh sách tôi gọi là "9 tool đòi mở rộng skill" thực tế liệt kê 11 tên; số đúng là 11.

Phương pháp đã sửa: phân loại theo hành vi (đọc / preview / provider-action / mutation), không theo HTTP method; đối chiếu alias theo token-set, không theo camelCase. Con số ở bảng trên là ứng viên, không phải danh sách enabled/disabled cuối — chốt danh sách cuối cần soát từng tool theo Quyết định 1.

Nhóm Chính sách (theo Quyết định 1 đã duyệt)
70 read (kể cả 7 qua POST) Giữ REST
1 provider action Lớp riêng, phân loại trước khi chốt
41 mutation map được Đi qua governed contract
49 mutation chưa map disabled tường minh, nêu lý do ở discovery + call. Không REST fallback

5b. Bảng mapping 41 mutation candidate map được (phương án B)

Đo bằng client.listTools() thật (schema quảng bá) + các args['…'] handler thật đọc trong ai-harness.ts.

stdio tool → skill Lệch args / defaults / result
create_item createItem 🔴 LỖI BASELINE (R12/S6): stdio gửi {...itemData, status} — field ra top level, thiếu envelope data ⇒ với payload thông thường REST createSchema trả 400 và service không được gọi. (Không phát biểu "chưa từng chạy được với mọi input": input mà data tình cờ là key của item vẫn thoả schema.) Ngoài ra status default 'draft' chỉ có ở stdio.
update_item updateItem 🔴 LỖI BASELINE (R13/S6): stdio gửi itemData bare ⇒ patchSchema strip key lạ ⇒ 200 OK nhưng patch rỗng — nội dung update bị bỏ qua, response là success-shaped no-op. (Không phát biểu "mất dữ liệu ngầm": probe không có DB nên không chứng minh dữ liệu đã lưu bị hỏng.) Đính chính: skill đã nhận status, nên vấn đề là envelope, không phải "thiếu status".
delete_item deleteItem Khớp (collection, id). confirm chỉ có ở stdio.
create_collection createCollection Mất 16 field: stdio advertise 18 property, skill chỉ đọc name + singleton. (S9 khoá số này.)
delete_collection deleteCollection name khớp. confirm chỉ có ở stdio.
delete_field deleteField Đổi tên key: field_namename. Mất force. confirm chỉ có ở stdio.
create_relation createRelation Pass-through: skill làm createRelation(args as never) — không đọc field nào, không validate. stdio advertise 13 property. Mapping phải tạo contract.
delete_relation deleteRelation id khớp. confirm chỉ có ở stdio.
create_role createRole Mất systemKey. Còn lại khớp (7 field).
delete_role deleteRole id khớp. confirm chỉ có ở stdio.
create_policy createPolicy Mất 5 field: enforceTfa, ipAllow, ipDeny, validFrom, validUntil — stdio advertise, skill không đọc. Đây là các field bảo mật.
delete_policy deletePolicy id khớp. confirm chỉ có ở stdio.
create_flow createFlow Khớp 6 field.
delete_flow deleteFlow id khớp. confirm chỉ có ở stdio.
create_intent createIntent Pass-through: skill không đọc field nào. stdio advertise 7 property gồm autonomyCap, budget, maintenanceWindow.
delete_intent deleteIntent id khớp. confirm chỉ có ở stdio.
create_webhook createWebhook Khớp 7 field.
update_webhook updateWebhook Pass-through: skill không đọc field nào.
delete_webhook deleteWebhook id khớp. confirm chỉ có ở stdio.
create_translation createTranslation Khớp 5 field.
update_translation updateTranslation Pass-through: skill không đọc field nào.
delete_translation deleteTranslation id khớp. confirm chỉ có ở stdio.
upsert_setting upsertSetting Khớp (key, value, scope).
delete_setting deleteSetting key khớp. confirm chỉ có ở stdio.
create_cdc_subscription createCdcSubscription Đổi tên 2 key: webhook_idwebhookId, extension_nameextensionName. Mất payload_mode.
delete_cdc_subscription deleteCdcSubscription Đổi tên key: stdio gửi id, skill đọc subscriptionId (handler còn đọc cả collection/data — dấu hiệu handler dùng chung, cần soát).
create_api_key createApiKey Khớp 4 field.
rotate_api_key rotateApiKey id/expiresAt khớp. confirm chỉ có ở stdio.
revoke_api_key revokeApiKey id khớp. confirm chỉ có ở stdio.
invite_user inviteUser email/roleId khớp.
update_user updateUser id/roleId/status khớp.
remove_user removeUser id khớp. confirm chỉ có ở stdio.
create_team createTeam name/description khớp.
delete_team deleteTeam id khớp. confirm chỉ có ở stdio.
add_team_member addTeamMember Đổi tên key: id (team id) → teamId.
remove_team_member removeTeamMember Đổi tên key: idteamId. confirm chỉ có ở stdio.
install_extension installExtension Khớp 8 field.
update_extension updateExtension Pass-through: skill làm const { id, ...patch } = args rồi forward patch không validate.
uninstall_extension uninstallExtension id khớp. confirm chỉ có ở stdio.
run_flow runFlow id/input khớp.
cdc_subscription_replay replayCdcSubscription Alias (token-set khớp, camelCase thuần không) — head trước xếp sai vào "chưa map". Còn phải map subscription_id/occurred_after và xử lý cursor mà skill chưa nhận.

Tổng hợp lớp lệch (đã cập nhật theo 41 mutation candidate):

  • 5 tool phải đổi tên key, không phải snake→camel: add_team_member/remove_team_member (idteamId), delete_field (field_namename), create_cdc_subscription (webhook_idwebhookId, extension_nameextensionName), delete_cdc_subscription (idsubscriptionId).
  • 5 tool skill nhận args mù (không có contract để "tuân theo", mapping phải tạo mới): create_relation (as never), create_intent, update_webhook, update_translation, update_extension.
  • Mất field nếu map thẳng: create_collection (16, trên 18 property advertise), create_policy (5, gồm enforceTfa/ipAllow/ipDeny/validFrom/validUntilfield bảo mật), create_role (systemKey), create_cdc_subscription (payload_mode), delete_field (force). Riêng update_item: không tính là mất status — skill đã nhận status, vấn đề nằm ở envelope.
  • 32 tool trên toàn registry có confirm: z.literal(true) (mẫu đếm: toàn registry, không chỉ nhóm mapped) mà skill không có khái niệm tương ứng — giữ lại theo Quyết định 5.
  • 1 default lệch: create_item.status = 'draft'.

⚠️ Hệ quả scope: phương án B không thuần cơ học. Đúng 11 tool (create_collection, create_policy, create_role, create_cdc_subscription, delete_field, update_item, create_relation, create_intent, update_webhook, update_translation, update_extension) đòi mở rộng hoặc định nghĩa mới skill trong ai-harness.ts để map đúng. Riêng create_policy đáng chú ý nhất vì các field bị rơi là điều kiện bảo mật (enforceTfa, IP allow/deny, cửa sổ hiệu lực) — map một phần ở đây là âm thầm nới lỏng policy, nên tôi đề nghị không cho phép map một phần với tool này.

5c. Readiness của 41 candidate — phát biểu theo đúng mức đã đo

Sửa hai lỗi của bản trước (review vòng 6): bảng ghi "nhóm A 19" nhưng danh sách chỉ có 16 tên; và câu "mọi cặp đều vướng result shape" là suy rộng quá mức đo được.

Nhóm Số tool Mức bằng chứng Việc còn phải làm
A — lệch args/field, không map thẳng được 16 đã đối soát từng tên (§5b) rename key, bổ sung field bị rơi, hoặc định nghĩa contract cho pass-through
B — args khớp theo §5b 25 đối soát tên; chưa kiểm result từng cặp cần kiểm result/status từng cặp trước khi kết luận "chỉ thiếu result mapping"

Nhóm A (16 tên, đếm lại): create_item, update_item, delete_field, add_team_member, remove_team_member, create_cdc_subscription, delete_cdc_subscription, cdc_subscription_replay, create_collection, create_policy, create_role, create_relation, create_intent, update_webhook, update_translation, update_extension.

Phát biểu an toàn vs phát biểu chưa đo được

  • Đã chứng minh: 0/41 candidate được chứng minh enable-ready end-to-end. Không có cặp nào đi hết chuỗi advertised schema → body → service → result mà không còn câu hỏi mở.
  • CHƯA đo: "41/41 đều đã chứng minh lỗi result". Bản trước ngụ ý điều này — sai. Thực tế đo được: S10 kiểm 4 delete handler (delete_collection, delete_item, delete_role, delete_field); R15 kiểm 4 skill (createCollection, createRole, deleteRole, deleteCollection). 8 điểm đo, không phải 41.
  • CHƯA đo: 25 tool nhóm B "chỉ" thiếu result mapping. Chưa kiểm result từng cặp nên không loại trừ được lệch khác.

Gate đề xuất — thu hẹp cho đúng

Giữ gate chuẩn hoá status/decision contract (executed / pending_approval / denied) trước hoặc cùng lúc với việc enable. Không ép mọi domain payload về một cấu trúc đồng nhất — khác shape của payload domain không tự nó là lỗi, và đó là chỗ bản trước nói quá.

Hai điểm đo được, nêu đúng phạm vi

S10 — fault-injection probe cho rủi ro migration, không phải lỗi production đang xảy ra. Delete handler của stdio bỏ qua hoàn toàn giá trị fulfilled và tự dựng câu khẳng định:

await client.delete(`/collections/${name}`);
return okText(`${resource} "${id}" deleted.`);

Payload tôi tiêm là payload tổng hợp mô phỏng một decision sau adapter, KHÔNG phải nguyên response governed hiện tại — toToolDecision không có field executed, MCP còn bọc thêm content/structuredContent/isError rồi JSON-RPC bọc result, và DELETE /collections/:name hiện trả 204 sau khi đã thực thi. Nên test không tái hiện "CMS live park → stdio báo deleted". Nó chứng minh: nếu một adapter tương lai đưa decision vào đúng đường này thì người dùng bị báo sai — rủi ro của bước migration.

R15 — cách handler BỌC kết quả không đồng nhất. Dùng shape thật của service (sửa theo review; bản trước dùng sentinel row cho mọi method nên kết luận "trả row trần" là artefact của mock):

Service Shape thật Skill bọc thế nào Kết quả
AccessService.deleteRole {deleted:true,id} pass-through {deleted:true,id}
SchemaService.deleteCollection {ok:true} bọc thêm {deleted:true,result:{ok:true}}

Cả hai service đều đã tự trả cờ, nhưng một skill bọc và một skill không — nên client phải bóc hai kiểu khác nhau cho cùng một loại hành động, trong cùng registry. Đó là phát biểu đúng mức; nó không kết luận cả 41 cặp không tương thích.

6. Phạm vi — sửa lại theo review (P1)

Head trước tôi viết "chuyển stdio write sang tools/call không thuộc #454". Sai, và tôi rút lại. Đó chính là acceptance chính của issue ("Define and implement the governed tool contract across HTTP and stdio"). Đẩy nó ra ngoài là tự thu hẹp issue tới mức mất mục tiêu.

Phạm vi đúng — giữ trong #454:

  1. Một nguồn schema chuẩn + mapping tường minh (phương án B).
  2. Validation trước approval và trước mutation, kiểm lại ở executeApproved/retry.
  3. Write/autonomy gate độc lập isDangerous (L0 không ghi, L1 cần approval).
  4. Governance trên CẢ HAI transport — tức là stdio write đi qua governed contract, kèm chính sách disabled cho tool chưa map. Không REST fallback.
  5. Lỗi trả về có cấu trúc; nói rõ không gian approval ID.

Tách thành dependency riêng (không phải "bỏ ra ngoài scope"): chỉ đúng một việc — dạy harness resolve capability qua PermissionService thay vì string-match auth.roles. Lý do tách: nó đổi ai làm được gì (privilege change), và đo được là nó chặn cả read cho API key (R7) lẫn user thường (R6). Nó phải là blocking dependency của #454, không phải hạng mục post-#454: #454 chỉ đóng được khi stdio thật sự chạy dưới governance, và điều đó bất khả thi khi mọi principal không phải admin bị deny.

Ma trận token / feature-flag / compatibility

Principal auth.roles Reach hôm nay qua stdio (REST) Reach nếu chuyển sang tools/call trước khi có capability resolution
API key [] RBAC đầy đủ theo PermissionService Deny mọi skill, kể cả read (R7)
User thường ['role_<nanoid>'] RBAC theo policy của role Deny mọi skill (R6)
Bootstrap/admin ['admin'] admin Full

Cộng thêm hai ràng buộc: contentOs.mcp default off → 404 ở phần lớn site (RT4); và stdio ghim một site cho cả process qua LUMIBASE_SITE_ID (§3a) nên client đa site hiện phải chạy nhiều process.

Thứ tự thực hiện đề xuất (tất cả trong #454, trừ mục 0)

Bước Việc Ghi chú
0 Capability resolution qua PermissionService Issue riêng, blocking. Cần owner sign-off vì là privilege change.
1 Canonical schema + alias/arg/result mapping (§5b) Trong #454
2 Validation trước approval/mutation + lỗi có cấu trúc Trong #454
3 Write/autonomy gate độc lập isDangerous Trong #454
4 stdio write → tools/call; tool chưa map → disabled Trong #454, cần bước 0 xong trước
5 Bổ sung/chuẩn hoá skill cho 11 tool mất field (§5b) Trong #454 theo Quyết định 4 (đã duyệt): mở rộng đúng, không map một phần

Cam kết giữ nguyên: không nâng quyền token, không sửa permission REST chung, không rename/deprecate tool, không đụng #344–347 hay lane A.

7. Đề xuất exact paths + wiring cho shared schema

packages/contracts (chưa có grant, đây là đề xuất):

  • packages/contracts/src/agent-tools/schemas.ts — Zod schema từng skill
  • packages/contracts/src/agent-tools/tool-map.ts — alias map (tên + args/defaults/result)
  • packages/contracts/src/agent-tools/index.ts — export công khai
  • packages/contracts/src/index.ts — thêm 1 dòng re-export

Wiring bắt buộc (mcp-server hiện KHÔNG phụ thuộc contracts):

  • packages/mcp-server/package.json@lumibase/contractsworkspace devDependency (vì bundle); Zod vẫn là dependency runtime
  • pnpm-lock.yaml
  • packages/mcp-server/tsup.config.tsnoExternal cho contracts (đã chốt: bundle, không external)
  • Kiểm packed artifact: mcp-server publish lên npm, nên workspace:* phải bundle hoặc là dependency đã publish. Cần npm pack + smoke cài từ tarball, nếu không package vỡ khi cài ngoài monorepo.

Các mục manifest/lockfile/build là shared → coordinator-owned, chờ grant theo pha.

Đã chốt ở §0b, không mở lại: dùng root export sẵn có của contracts (không tạo subpath mới), bundle vào artifact mcp-server (workspace devDependency + tsup noExternal), chốt bằng pack/install ngoài monorepo. Phương án external-dependency và phương án đặt schema trong apps/cms + endpoint discovery đều đã bị loại; ghi lại chỉ để giữ dấu vết lịch sử. Chưa cần grant contracts/package.json, contracts/tsup.config.ts hay mcp-server/tsconfig.json — sẽ trả delta nếu build/pack chứng minh cần.

Hai mục bổ sung theo báo cáo review mà proposal cũ của tôi thiếu: apps/cms/src/services/access-service.ts (pha implementation, cho role/policy parity + domain guard) và packages/mcp-server/src/client.ts phải đọc result/error của JSON-RPC thay vì request() vốn unwrap json.data.

Không đụng: .kiro/specs/**, CHANGELOG.md, AGENTS.md, workflows, packages/database/**, docs EN/VI, file lane A, và #344–347.

8. Bằng chứng test — gắn SHA cho từng lần đo

Sửa lỗi của bản trước (review vòng 6): §8 từng gắn "hook 14/14, exit 0, không --no-verify" vào 50b9810e, dù commit đó dùng --no-verify và full test là 13/14. Bằng chứng lịch sử được gắn SHA rõ ràng bên dưới.

Tại head hiện tại f900936188ef56a08f17127800df1a4e58ac8871 — Node v26.0.0, gọi executable trực tiếp trong package (tránh wrapper pnpm)

Kiểm tra Lệnh Kết quả
CMS repro + parity + backstop node node_modules/vitest/vitest.mjs run … trong apps/cms 38/38 pass, 3 file, exit 0
CMS g2-mcp-contract-repro như trên, 1 file 23 pass, exit 0
MCP cả package node node_modules/vitest/vitest.mjs run trong packages/mcp-server 43/43 pass, 3 file, exit 0
MCP typecheck node node_modules/typescript/bin/tsc --noEmit exit 0
CMS typecheck node node_modules/typescript/bin/tsc --noEmit trong apps/cms exit 0, 0 dòng output — lặp lại 2 lần. Xác nhận process.version = v26.0.0, tsc 5.9.3. Đây là kết quả bạn chưa lấy được do ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY; gọi binary trực tiếp thì không gặp.
pre-commit hook đầy đủ check:all + pnpm test toàn workspace 14/14 task, exit 0, không --no-verify
check:all (nửa guard rẻ của hook) exit 0

Lịch sử --no-verify tại 50b9810e

Full workspace test đỏ với đúng một test fail: "every minted code is XXXX-XXXX" (modules/setup/backup-codes-persister, 30017ms) — không phải file của PR này.

Điều tôi đã đo: stash toàn bộ thay đổi rồi chạy riêng file đó trên cây sạch → 7/7 pass, exit 0. Lần chạy trước là flow-service.test.ts, cũng pass khi chạy riêng.

Đối chứng đã đo (làm theo yêu cầu của review — đây là bằng chứng lần trước còn thiếu). Chạy full CMS suite bằng binary trực tiếp, Node 26, hai cấu hình:

Cấu hình Kết quả
CMS suite file repro của PR 321 file / 2712 test / 0 fail, exit 0
CMS suite LOẠI TRỪ file repro của PR (--exclude) 320 file / 2689 test / 0 fail, exit 0
CMS suite qua pnpm test (turbo, 14 task song song) 4 test timing fail

Nghĩa là: package CMS xanh cả hai chiều khi chạy riêng, còn 4 failure được quan sát trong lần chạy song song nhiều package. Kết quả này phù hợp với giả thuyết có tương tác timing/concurrency ở mức workspace, nhưng chưa xác nhận quan hệ nhân quả và chưa loại trừ tương tác giữa diff với tải song song. Muốn kết luận mạnh hơn cần so full-workspace trên base và head dưới điều kiện tương đương, lặp lại đủ lần.

Điều vẫn CHƯA chứng minh: tôi chưa chạy pnpm test trên chính commit base để khẳng định failure đã tồn tại ở base dưới đúng điều kiện đó. Dựng worktree thứ hai cần node_modules riêng nên tôi chưa làm. Vì vậy tôi không phát biểu "đã tồn tại trên base"; chỉ phát biểu điều đã đo ở trên.

Bằng chứng lịch sử — gắn SHA, không áp cho head hiện tại

SHA Kiểm tra Kết quả
d9dbcafa pre-commit hook đầy đủ 14/14 task, exit 0, không --no-verify
ca29c1db pre-commit hook đầy đủ 14/14 task, exit 0, không --no-verify
86373630 (lịch sử) pre-commit hook đầy đủ 13/14, exit 1 → dùng --no-verify
50b9810e (lịch sử) pre-commit hook đầy đủ 13/14, exit 1 → dùng --no-verify
f9009361 (head hiện tại) pre-commit hook đầy đủ 14/14, exit 0, không --no-verify

Cải thiện thời gian test (không nới testTimeout)

Test Trước Sau Cách sửa
RT1 18656 ms 129 ms import router một lần ở module scope, bỏ vi.resetModules() mỗi test
S7 5179 ms 2 ms registry dùng chung dựng một lần thay cho 8 lần registerAllTools

Kiểm âm (chứng minh test là hàng rào, không phải phát biểu rỗng)

Kiểm âm Kết quả
R14: đổi 1 skill thành tên không tồn tại đỏ đúng, thông báo rõ
GP1/GP4/GP5: đặt enableAgentHarnessAudit=false 3 fail đúng mong đợi
S7: khi viết, hai target tôi đoán sai bị bắt query_insights thật đi /dashboards/:id/panels/preview; run_panel nhận dashboardId

CI tại head này

Core CI pass. Riêng CodeQL Analyze (actions) FAILURE — tôi không gọi toàn bộ CI là xanh.

Điều tra: bước đỏ là "Uploading results" (upload SARIF), sau khi export SARIF đã thành công (Exported results to SARIF (131ms)); các bước Initialize CodeQL và extract đều success. Đối chiếu lịch sử: cùng workflow xanhca29c1db, d9dbcafa, be20124d, 0585cbc3 và trên main, chỉ đỏ ở head này. Và git diff ca29c1db..86373630 --name-only cho 0 file YAML/workflow (chỉ hai file test .ts), trong khi language actions của CodeQL chỉ phân tích workflow — nên input của phân tích đó giống hệt lần xanh.

Không rerun được run cũ (This workflow run cannot be retried — CodeQL default setup), nhưng push 50b9810e đã tạo run mới làm phép thử tự nhiên:

Run Head Analyze (actions)
34750571662 86373630 failure
34755181716 50b9810e success

transient ở tầng upload SARIF, không phải regression từ PR. Finding này đã tự đóng ở head hiện tại. CI đầy đủ tại 50b9810e: CI success · Docker success · CodeQL success (cả 3 job). Tôi không claim mạnh hơn — log không có error line chỉ đúng nguyên nhân upload thất bại.

9. Chưa verify / giới hạn

  • Chưa có MCP client thật nói chuyện với CMS thật. S1/S3/S4 dùng client/server MCP thật nhưng CMS là recorder; RT1RT4 dùng route + harness thật nhưng db là fake. Chưa có chuỗi list → call → deny/pending/execute → approval trên hệ thống sống.
  • Chưa có bằng chứng DB. GP*/RT* đếm row trên fake. Không có *.db.integration.test.ts (reserved cho test-harness: các suite .db.integration báo "passed" khi không kết nối được DB #427, chưa xin grant). approvalIdRT3 do fake returning() sinh ra: chứng minh id được propagate, không chứng minh id resolve ở decision endpoint.
  • Cross-tenant và retry/duplicate: chưa. site_1 là fake nên không chứng minh isolation.
  • Bảng mapping §5b đối chiếu schema quảng bá thật với args handler thật, nhưng chưa kiểm tương đương ngữ nghĩa từng field khi chạy (ví dụ createRelation nhận args as never nên không có contract để so).
  • RT đặt trong file repro services vì grant bước 1 chỉ gồm hai file; đúng ra thuộc routes/__tests__/. Cần grant nếu muốn chuyển.
  • DoD chờ verifier, không phải đã pass. Các test hiện cố ý assert hành vi sai; khi sửa phải đổi thành regression assertion, và không được coi chúng là bằng chứng G2 đã đạt.

Reproduction only for #454 (G2) — no implementation change. Each case
asserts the CURRENT behaviour so a later fix flips it deliberately.

apps/cms (R1-R10):
- tools/list advertises a bare {type:'object'} for every core skill
- ToolRegistryService.coreTool() discards the inputSchema the seven
  version/appspec skills already declare
- createItem / createCollection reach their service with undefined
  collection/name; deleteItem books an approval from empty arguments
- harness capabilities are auth.roles (a role id, or [] for API keys),
  so non-bootstrap principals are denied every skill while REST
  authorizes the same token through PermissionService

packages/mcp-server (S1-S4):
- every stdio tool is a plain REST call; the harness (kill switch,
  autonomy, HITL, veto, agent_runs audit) is never entered
- a dangerous stdio call returns no approval id
- stdio has per-field Zod schemas that HTTP MCP lacks; names are
  snake_case REST verbs disjoint from the camelCase skill registry

Refs #454

@khuepm khuepm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Reviewer — B-02 / #454 / PR #471

Head kiểm: 7cf548c068263ef76d553472027682eb129cca92; base 6a20441af5dde899b976479f0ed7f8d1a9341dee.
Kết luận: CẦN SỬA bản audit/repro và contract; giữ Draft, chưa cấp grant implementation. Đây là review bước 1, không phải yêu cầu PR chỉ có test phải hoàn thành toàn bộ G2 ngay.

Điểm đã xác nhận: coreTool() ghi đè inputSchema sau spread; McpService quảng bá object rỗng; các stdio tool được probe gọi REST riêng. Hướng một nguồn schema chuẩn là đúng. Nhận định parity test cũ không chứng minh HTTP/stdio parity cũng đúng, nhưng test đó vẫn có giá trị kiểm adapter, không nên bỏ.

Các điểm phải sửa:

  1. [P1 — thiếu repro/contract] Content write L0/L1 chưa được kiểm ở governed path. R3 đặt enableAgentHarnessAudit: false; R4 gọi thẳng runSkill; R5 cũng tắt audit. Route MCP production truyền services và bật nhánh governed mặc định (ai-harness.ts:2038–2047,2142–2143). Tại baseline, createItem được xếp safe, còn AutonomyService.resolve chỉ nằm trong if (isDangerous) (:2288–2294); do đó chỉ chuyển stdio sang HTTP và thêm validation chưa giải quyết acceptance “L0 không ghi, L1 cần approval” cho content write. Bổ sung repro dùng nhánh governed với input hợp lệ, L0/L1, đếm service calls/approval decisions; ghi rõ phần unit fake và phần cần DB thật. Contract phải có write/autonomy gate độc lập với dangerous classification, giữ read behavior. Đây là gap của kế hoạch, chưa phải regression do hai file test này tạo ra.

  2. [P1 — contract] Không duyệt REST fallback cho các tool ghi chưa có skill mapping. §4.5 đề xuất để tool không map tiếp tục gọi REST và chỉ gắn nhãn ungoverned; nhãn không thực thi L0/HITL/kill switch. Cần inventory đủ các write tool với mapping hoặc trạng thái disabled rõ ràng; không fallback sang REST khi MCP disabled/deny/pending/network error. Đồng thời stdio hiện dùng API key, HTTP nhận roles: [] và từ chối các core skills đang probe; chỉ đổi endpoint sẽ làm hỏng call đang dùng được, ngay cả read. Chốt ma trận token/feature-flag/compatibility và đường chuyển đổi tường minh. Không nâng token lên admin/*, không sửa quyền REST chung để né lỗi. Không được coi “ghi chú compatibility” là đã đạt acceptance của #454.

  3. [P2 — bằng chứng] R5 không tạo approval thật hoặc chứng minh approval ID dùng được. Fake returning() luôn trả apr_repro_1, không lưu DB, không gọi approval route; R4 không đi qua executeApproved. Sửa test title/comment và PR/#454 từ “real row/usable ID/after approval” thành “legacy insert attempt/direct handler probe”; bổ sung governed-path probe cho thứ tự validate/approval. DB roundtrip thật vẫn thuộc gate G1/G2 sau này. R7/R8 chỉ chứng minh capability helper/legacy path với mảng role giả, không chứng minh cùng token thực có toàn quyền stdio. Ma trận “stdio không có admin backstop” cũng phải sửa: các REST prefix access/api-keys/agent/permissions/roles/settings… có withControlPlaneAccessGuard; riêng collection route dùng schema permission. Phân biệt theo tool, không tổng quát toàn transport.

  4. [P2 — test thiếu assertion] S3 chỉ kiểm tên key schema; S1/S4 chỉ probe tập mẫu. S3 vẫn xanh nếu schema value thành z.any() hoặc SDK không validate; hãy dùng official MCP server/client trong memory để gửi input thiếu/sai kiểu và assert handler/client mutation không chạy (không cần DB live). Nếu chưa làm, đổi tên và giới hạn claim thành “đăng ký schema metadata”. R10/S4 không kiểm toàn bộ giao của hai registry, nên bỏ “full list asserted/zero overlap” như kết quả test đầy đủ hoặc bổ sung inventory toàn bộ. Các giới hạn phải nằm ngay cạnh kết quả, không chỉ cuối PR.

Quyết định coordinator:

  • Chọn B: giữ tên public của cả hai transport, mapping tường minh sang canonical tool ID + schema. Một nguồn contract không bắt buộc một cách viết tên. Mapping phải bao gồm chuyển đổi args/defaults/result, không chỉ snake_case → camelCase; ví dụ confirm/status/collection metadata hiện khác nhau. Chưa deprecate hoặc rename bất kỳ tool/endpoint nào; không mở ticket rename chỉ để giải quyết #454.
  • Đồng ý về vị trí packages/contracts ở mức thiết kế, chưa cấp write grant toàn thư mục. B trả danh sách file cụ thể cho schema + alias map + export và dependency/build wiring: mcp-server hiện chưa phụ thuộc contracts, nên proposal không thể bỏ qua manifest/lockfile/packed-artifact verification. Chỉ cấp grant sau G1 acceptance và sửa contract; không coi comment này là quyền sửa các shared files.
  • Validation phải xảy ra trước tạo approval và trước domain/service mutation, và được kiểm lại khi executeApproved/retry. Không buộc xóa audit của request bị từ chối: nếu giữ denial audit, phân biệt rõ với business mutation và không tạo orphan run đang running. Đề xuất “trước appendToolCall” cần nói cả ensureRun đã chạy trước đó.
  • Trong bước audit hiện tại, cho phép cập nhật hai file repro đang có và PR/#454 để xử lý các finding; chưa sửa implementation. Giữ checkout/nhánh hiện có, không cần rename. #453 vẫn chưa có reviewer acceptance mới; trạng thái issue OPEN một mình không là bằng chứng runtime hỏng, CI/merge một mình cũng không thay acceptance.
  • PR title/body và phản hồi implementer cần tiếng Việt theo quy ước #331.

Bằng chứng reviewer chạy (Node v26.0.0, thuộc engines hỗ trợ):

Kiểm tra Kết quả
CMS: g2-mcp-contract-repro + mcp-parity.property 2 files, 16/16 pass, exit 0
MCP server: full package vitest 3 files, 36/36 pass, exit 0
git diff --check base...head pass, exit 0
GitHub CI tại head kiểm Các check hiển thị SUCCESS; không thay bằng chứng MCP client/DB riêng

DoD verifier:

  • §1: diff chỉ hai file test, không production change; focused tests reviewer xanh. Full workspace typecheck/test/build là trạng thái CI, không phải reviewer chạy lại; không xác nhận lại nguyên nhân 11 timeout mà implementer báo trên Node cũ.
  • §2 setup: test-only không đổi bootstrap/backfill; n/a vòng này. G2 implementation phải đánh giá bật MCP flag, token/role setup và registry impact trước nghiệm thu.
  • §2b tenant: fake site_1 không chứng minh isolation; real two-site, permission/field/row, retry/duplicate checks còn pending cho feature.
  • §2c guard: không đổi route trong diff; bảo toàn backstop/RBAC là gate cho proposal mới. §2d shell: không đổi Studio/auth/CORS trong diff, n/a. §2e dependency: không đổi dependency hiện tại, n/a; shared-schema wiring cần grant/pack checks sau.
  • §3 spec: đúng trạng thái audit/Draft, nhưng contract chưa chốt. §4/4a docs: không đổi docs/en/vi, không phát hành/API change trong PR test-only; n/a hiện tại. EN/VI contract docs và changelog/upgrade impact phải có khi implementation thay behavior.
  • §5 tutorial: chưa đổi runtime/tutorial, n/a vòng này; phải rà compatibility khi đổi stdio calls.
  • §6: bug-characterization tests hiện cố ý assert behavior sai; phải chuyển thành regression assertions khi sửa, không merge chúng như bằng chứng G2 đã đạt. Chưa cần sửa DoD chung.
  • §7: các finding thuộc #454, không tạo issue trùng. B32 là tham chiếu implementer cung cấp, không có xác minh mới trong review này.

Chưa chạy real MCP client, disposable DB approval roundtrip hoặc live cross-tenant test. Chưa nghiệm thu G1/G2; không merge/publish/đóng issue.

}),
} as unknown as Database;

const harness = new AISecureHarness({ db, siteId: 'site_1', enableAgentHarnessAudit: false });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Reviewer — [P1] R3/R5 tắt governed path; R4 gọi runSkill trực tiếp. Các probe này chưa khóa acceptance content write L0/L1 trên đường HTTP thực. Thêm repro với enableAgentHarnessAudit=true, valid createItem, L0/L1 và assertion service/approval; production hiện chỉ resolve autonomy trong nhánh dangerous. Giữ nhãn unit/fake rõ ràng.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Đã xử lý ở head e6633ce4.

Thêm block G2 repro · governed path gồm GP1GP5, dùng enableAgentHarnessAudit: true (đúng nhánh routes/mcp.ts dùng) trên một fake Database table-aware đủ để chạy hết chuỗi kill switch → ensureRun → appendToolCall → getTool → policy → risk → runSkill → finishToolCall → closeRun.

  • GP1 chốt "nhánh governed thật sự đang chạy" bằng cách assert có agent_runs + agent_tool_calls (legacy không tạo cả hai). Đã kiểm âm: đổi thành enableAgentHarnessAudit: false thì GP1/GP4/GP5 đỏ đúng, nên case không thể âm thầm tụt về nhánh anh chỉ ra.
  • GP2 autonomyCap: 0 (L0) và GP3 autonomyCap: 1 (L1): vẫn ghi content, 0 ai_approvals, 0 agent_approvals. Cả hai tự assert thêm agent_runs + toolCallId để độc lập chứng minh governed-ness.
  • GP4 control: cùng autonomyCap: 0 nhưng deleteItem (dangerous) thì park với 1 + 1 approval. Cặp GP2/GP3 vs GP4 là bằng chứng gate gắn vào isDangerous, không gắn vào "là write".

Xác nhận đúng như anh nói: production chỉ resolve autonomy trong nhánh dangerous (:2288-2294), dưới đó rơi xuống Step 4: Safe skill — execute directly (:2456). Nên contract §4 nay có thêm yêu cầu write/autonomy gate độc lập với isDangerous, giữ nguyên hành vi read — và tôi đã ghi rõ trong PR rằng chỉ chuyển stdio sang HTTP + thêm validation là chưa đạt acceptance L0/L1.

Nhãn evidence: có block EVIDENCE CLASSES ở đầu file phân biệt fake / legacy / governed / MCP thật.

// human a real approval ID for arguments that can never succeed.
// EXPECTED: invalid input is rejected before any approval is written.
expect(result.status).toBe('pending_approval');
expect(result.approvalId).toBe('apr_repro_1');

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Reviewer — [P2] apr_repro_1 là ID fake trả cứng ở dòng 158; test không tạo DB row thật hay dùng approval route. Sửa claim real/usable approval trong comment + PR/#454, phân biệt insert attempt với DB roundtrip; runSkill trực tiếp ở R4 cũng chưa chứng minh after-approval.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Đúng, và đã sửa ở head e6633ce4.

R5 đổi tên thành "deleteItem with NO arguments still ATTEMPTS an approval insert (legacy path, fake db)". Hằng số đổi từ apr_repro_1 thành fake_not_a_db_id để không ai đọc nhầm là id thật, kèm comment SCOPE OF THIS EVIDENCE liệt kê tường minh 4 điều test không chứng minh:

  1. không có row ai_approvals thật (không lưu gì),
  2. không có approval id dùng được (id hard-code ngay trên đó),
  3. không chứng minh id resolve ở decision endpoint (không gọi route nào),
  4. không có after-approval execution (executeApproved không hề được gọi).

R4 cũng đổi tên: bỏ "after approval", thành "reaches SchemaService via runSkill", vì nó gọi runSkill trực tiếp.

Câu hỏi thứ tự validate/approval nay được GP4/GP5 phủ trên nhánh governed. Riêng GP5 tôi ghi luôn ghi chú của anh: ensureRun đã chạy trước appendToolCall, nên "validate trước appendToolCall" là cần nhưng chưa đủ — fix còn phải không để lại run running mồ côi, và nếu giữ audit cho request bị từ chối thì phải phân biệt rõ với business mutation.

DB roundtrip thật vẫn để nguyên là gate G1/#453 + DB dùng một lần, không claim trong PR này.

Về R7/R8: đã siết. Test cũ R8 (giờ là R7) đổi tên thành "an API-key-shaped capability set (roles: [])", nói rõ là feed hình dạng withAuth sinh ra chứ không authenticate key thật, và bỏ khẳng định "cùng token đó có toàn quyền stdio" — thay bằng phát biểu đúng phạm vi: điểm được chứng minh bằng source chỉ là hai model authorization không liên quan nhau (PermissionService.canAccess vs string membership trên auth.roles), và hệ quả là không được giả định việc chuyển sang tools/call là trong suốt với caller API key.

Ma trận backstop cũng sửa theo đúng ý anh: không còn phát biểu toàn transport. Thêm test S5 chứng minh backstop là per-prefixdelete_role, revoke_api_key, delete_flow nhắm vào prefix có withControlPlaneAccessGuard; còn /collections/items thì không, và đó mới là chỗ bất đối xứng thật.

// has real per-field schemas that HTTP MCP lacks, and HTTP MCP has harness
// governance that stdio lacks.
const createItem = tools.get('create_item')!;
expect(Object.keys(createItem.config.inputSchema ?? {}).sort()).toEqual([

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Reviewer — [P2] Assertion chỉ kiểm Object.keys, không chứng minh Zod/SDK từ chối input. z.any() hoặc bypass validation vẫn xanh. Thêm invalid-call test qua official MCP server/client in-memory và assert handler không chạy, hoặc giới hạn tên/claim S3 thành schema metadata được đăng ký.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Đã làm theo phương án anh đề xuất (không hạ claim, mà đi kiểm thật) — head e6633ce4.

S1, S3, S4 nay dựng Client + McpServer thật của @modelcontextprotocol/sdk nối bằng InMemoryTransport.createLinkedPair(), chạy registerAllTools thật. Không cần DB, không cần network. Chỉ LumiBaseClient bị thay bằng recorder, nên "tới CMS" nghĩa là "handler đã phát REST call".

S3 gửi 3 input sai qua client thật:

  • thiếu hẳn field bắt buộc ({}),
  • sai kiểu (collection: 123, data: 'not-an-object'),
  • vi phạm confirm: z.literal(true) trên delete_item.

Assertion quyết định là expect(calls).toEqual([]) — không handler nào chạy, nên đúng cái anh yêu cầu: z.any() hoặc validator bị bypass sẽ cho handler đi qua và ghi call vào recorder ⇒ test đỏ.

Ngoài ra không dừng ở "có lỗi" mà assert lý do: mỗi ca phải là JSON-RPC -32602 + Input validation error + tên field cụ thể. Chuỗi thật quan sát được:

MCP error -32602: Input validation error: Invalid arguments for tool create_item:
Invalid input: expected string, received undefined at collection
Invalid input: expected record, received undefined at data

expected true at confirm cho delete_item. Có thêm một control ở cuối: cùng tool với input hợp lệ phát đúng 1 POST /items/posts, chứng minh 3 ca trên bị loại vì input chứ không vì plumbing.

S4 bỏ tập mẫu: gọi client.listTools() thật rồi duyệt toàn bộ registry theo bất biến snake_case (0 tên chứa chữ hoa). Phía CMS, R9 (cũ R10) duyệt toàn bộ registry HTTP theo bất biến camelCase (0 tên chứa _). Hai bất biến đó cùng nhau chứng minh disjoint mà không cần cross-import — hai package không phụ thuộc nhau nên không thể lấy giao thật mà không đổi manifest, và tôi đã ghi giới hạn đó ngay tại chỗ. Cụm "full list asserted / zero overlap" cũ đã bỏ.

Giới hạn được đặt cạnh từng kết quả (comment SCOPE/EVIDENCE CLASSES trong file) chứ không chỉ ở cuối PR.

…laim

Xử lý 4 finding của reviewer trên PR #471 (#454). Vẫn chỉ là repro,
không sửa implementation.

[P1] Thêm nhánh governed (GP1–GP5), không tắt harness audit:
- GP1 chứng minh nhánh governed thật sự chạy (có agent_runs +
  agent_tool_calls); kiểm âm: đặt enableAgentHarnessAudit=false thì
  GP1/GP4/GP5 đỏ đúng
- GP2/GP3: autonomyCap 0 (L0) và 1 (L1) vẫn ghi content, 0 approval
- GP4 control: cùng cap nhưng skill dangerous thì park → chứng minh
  gate gắn với isDangerous, không gắn với "là write"
- GP5: ensureRun + appendToolCall ghi trước mọi kiểm input

[P2] Siết claim về approval:
- R5 đổi tên thành "ATTEMPTS an approval insert (legacy path, fake db)";
  id đổi thành fake_not_a_db_id và liệt kê tường minh những gì test này
  KHÔNG chứng minh (row thật, id dùng được, decision endpoint,
  executeApproved)
- R4 đổi tên: "via runSkill", bỏ chữ "after approval"

[P2] Siết claim capability + backstop:
- R7 (cũ R8) giới hạn vào "capability set hình dạng API key", bỏ khẳng
  định cùng token có toàn quyền stdio
- S5 mới: backstop là per-prefix, không per-transport — delete_role,
  revoke_api_key, delete_flow nằm sau withControlPlaneAccessGuard;
  /collections và /items thì không

[P2] Validation và coverage thật:
- S1/S3/S4 nay chạy MCP Client + McpServer thật qua InMemoryTransport
- S3 assert JSON-RPC -32602 "Input validation error" kèm tên field và
  quan trọng nhất: 0 REST call → handler không chạy; z.any() sẽ làm đỏ
- S4 duyệt toàn bộ registry (bất biến snake_case), R9 duyệt toàn bộ
  registry HTTP (bất biến camelCase) thay cho tập mẫu

Thêm khối EVIDENCE CLASSES ở đầu cả hai file, nêu rõ đâu là fake,
đâu là MCP client thật, và những gì vẫn phải chờ gate DB/#453.

Refs #454
@khuepm khuepm changed the title test(mcp): G2 contract audit and reproduction for tool schemas + transport governance test(mcp): audit + repro G2 cho tool schema và governance nhất quán giữa hai transport Sep 12, 2026
Tự rà sau khi đối chiếu lại handoff bước 1 (#454): phía stdio đã có MCP
client thật, còn phía HTTP chỉ kiểm ở tầng service — chưa tái hiện đúng
chuỗi "list-tools → call" mà handoff yêu cầu. Vẫn chỉ là repro, không
sửa implementation.

Thêm RT1–RT4 chạy chính handler POST /api/v1/mcp qua Hono, với
AISecureHarness / ToolRegistryService / McpService THẬT; chỉ db và
runtime binding là fake:
- RT1: tools/list end-to-end advertise {type:'object'} cho mọi tool
- RT2: client tuân thủ tools/list vẫn không tạo được call hợp lệ — {}
  thoả contract quảng bá, gửi đi thì KHÔNG bị -32602
- RT3: quyết định governed (pending_approval + approvalId) đi trong tool
  result, không phải protocol error
- RT4: contentOs.mcp off (mặc định) thì cả surface 404 MCP_DISABLED

Hai phát hiện mới, sẽ đưa vào contract:
- RT2 cho thấy lỗi rò nguyên văn ra client: message là TypeError
  "Cannot read properties of undefined (reading 'length')" từ trong
  ItemService thật, không phải lỗi validation nêu tên field. Test pin
  luôn chỗ rò này để bản fix không âm thầm giữ nguyên.
- RT3 cho thấy id trả cho client là id của agent_approvals (do
  toToolDecision ưu tiên agentApprovalId), trong khi execute() insert cả
  hai bảng. Hai không gian id do hai endpoint khác nhau quyết định
  (/agent/approvals/:id/decide dùng agentApprovals.id,
  /ai/approvals/:id/decide dùng aiApprovals.id) — "approval ID dùng
  được" phải nói rõ client nhận id nào và endpoint nào nhận.

RT đặt trong file repro sẵn có vì grant bước 1 chỉ gồm hai file này;
thêm file thứ ba dưới routes/__tests__/ sẽ cần grant mới.

Refs #454
Xử lý finding P2 của review vòng 2 trên PR #471 (#454). Vẫn chỉ repro,
không sửa implementation.

S5 trước đây viết "/collections và /items là các prefix mà HTTP MCP
admin-gate thêm bằng backstop của mcp.ts". SAI cho createItem:
isControlPlaneSkill(createItem) trả false (items:write không phải
schema:* mutating, tên không bắt đầu delete), nên KHÔNG bên nào
admin-gate nó. Test vẫn xanh vì chỉ so danh sách REST prefix — danh
sách đó giống nhau ở cả hai chiều nên không thể phát hiện kết luận sai.

Sửa:
- S5 nay dùng deleteItem + deleteCollection để chứng minh bất đối xứng
  cả hai mặt (admin gating + agent governance), và ghi rõ createItem chỉ
  bất đối xứng ở agent governance
- Bỏ câu quy backstop theo prefix; nói rõ backstop quyết định theo SKILL
  qua isControlPlaneSkill, không theo prefix
- Thêm R11 phía CMS assert trực tiếp isControlPlaneSkill cho
  deleteItem/deleteCollection (true) và createItem/updateItem/listItems/
  listCollections (false). Đây là chỗ duy nhất import được classifier vì
  packages/mcp-server không phụ thuộc apps/cms.

R11 chính là hàng rào mà phiên bản cũ thiếu: nó đỏ ngay nếu ai đó lại
viết createItem là control-plane.

Refs #454
Xử lý 2 thiếu sót audit của review vòng 3 trên PR #471 (#454). Vẫn chỉ
repro, không sửa implementation.

[P1] Lỗi envelope body — lớp lỗi mà bảng mapping cũ KHÔNG THỂ thấy, vì
nó chỉ so "schema quảng bá" với "args skill đọc", không so body REST
thật sự gửi đi. Tách hai nửa vì hai package không phụ thuộc nhau:
- S6 (stdio): create_item spread field ra top level
  ({title,status}), update_item gửi bare ({title}) — cả hai thiếu
  envelope `data`
- R12 (cms): đúng body đó qua itemsRouter thật -> 400 VALIDATION,
  ItemService KHÔNG được gọi. create_item của stdio chưa từng chạy được
  với contract nó tự quảng bá
- R13 (cms): body update_item bare -> 200 OK nhưng ItemService.patch
  nhận {} vì patchSchema strip key lạ. Nguy hơn R12: client tưởng cập
  nhật thành công trong khi không có gì đổi
Cả hai có control case bọc đúng envelope để chứng minh nguyên nhân là
envelope, không phải plumbing.

[P2] Sửa phân loại inventory:
- S7: 7 tool non-GET có ngữ nghĩa đọc/preview (check_permission,
  check_access_conflicts, dry_run_access_import, diff_schema, lookup_tm,
  query_insights, run_panel) + 1 hành động provider (translate_text)
  => 98 là số handler khác GET, KHÔNG phải 98 mutation
- S8: cdc_subscription_replay là ALIAS của replayCdcSubscription.
  camelCase thuần cho cdcSubscriptionReplay nên bỏ lọt; đối chiếu bằng
  token-set thì khớp. Kèm kiểm âm để test không thành phát biểu rỗng
- S9: create_collection advertise 18 property (không phải 19) nên skill
  thiếu 16 (không phải 17); 32 tool toàn registry có confirm, nêu rõ
  mẫu đếm thay vì "~20"

Số đúng: 161 tool = 70 read (63 GET + 7 POST-đọc) + 1 provider action
+ 90 mutation; mutation chia 41 map được (40 tên + 1 alias) / 49 chưa.

Refs #454
Xử lý 2 finding review vòng 4 trên PR #471 (#454). Vẫn chỉ repro, không
sửa implementation.

[P2 test reliability] RT1 timeout 15s ở cấu hình chuẩn.
Nguyên nhân là chi phí IMPORT, không phải tranh tài nguyên: buildApp gọi
vi.resetModules() + await import('../../routes/mcp') ở MỖI test, nên lần
đầu phải cold-import cả cây route và tính vào budget của RT1. Đo được:
RT1 18656ms so với 214-357ms của RT2/RT3/RT4.
Sửa bằng cách bỏ re-import — hoisted vi.mock cho feature-flags với
flagState mutable, import router MỘT LẦN ở module scope, buildApp chỉ
dựng Hono. KHÔNG nới testTimeout.
Kết quả: RT1 18656ms -> 129ms. Lệnh chuẩn của reviewer chạy 2 lần đều
36/36 pass exit 0 (không --testTimeout override). RT4 vẫn lật được flag
qua flagState nên phần compatibility gate giữ nguyên ý nghĩa.

[P2 evidence scope] S7/S8 trước đây nói quá phạm vi:
- S7 chỉ assert 8 tên tồn tại + độ dài 2 mảng literal, nên đổi handler
  một tool đọc thành write vẫn không đỏ. Nay GỌI handler thật và khoá
  REST target (method + path) của cả 8 tool. Test này đã tự chứng minh
  là hàng rào: nó bắt đúng 2 chỗ tôi đoán sai (query_insights thật ra đi
  /dashboards/:id/panels/preview, run_panel nhận dashboardId chứ không
  phải id). Ghi rõ nó KHÔNG chứng minh ngữ nghĩa read — phán đoán đó đến
  từ đọc routes CMS, có dẫn nguồn.
- S8 chỉ chuẩn hoá chuỗi literal, xoá skill phía CMS vẫn không đỏ. Nay
  giới hạn claim đúng phạm vi "thuật toán tìm CANDIDATE", và ghi rõ
  "không trùng token" KHÔNG suy ra "không có skill tương đương".
- Thêm R14 phía CMS: mọi skill trong bảng mapping 41 ứng viên phải tồn
  tại trong CORE_SKILLS thật, gồm ca alias replayCdcSubscription. Kiểm
  âm: đổi một tên thành renamedAwaySkill -> đỏ đúng, thông báo rõ.

[Diễn đạt] Siết claim R12/R13 theo đúng probe:
- R12 chỉ chứng minh payload thông thường bị từ chối, KHÔNG phải
  "create_item chưa từng chạy được với mọi input".
- R13 chỉ chứng minh update bị bỏ qua + response success-shaped no-op,
  KHÔNG chứng minh dữ liệu cũ trong DB bị xoá.

Refs #454
Xử lý review nghiệm thu tại ca29c1d (#454) và đóng khoảng trống cuối của
bảng mapping. Vẫn chỉ repro, không sửa implementation.

[Reliability] S7 fail lần đầu ở máy reviewer (5179ms so với testTimeout
mặc định 5000ms của package — package này không có vitest.config), pass
lần chạy lại. Nguyên nhân là chi phí ĐĂNG KÝ: S7 gọi registryOnly() 8
lần, mỗi lần registerAllTools dựng ~161 tool kèm Zod schema. Sửa bằng
registry dùng chung dựng đúng một lần + helper callToolIsolated xoá
recorder giữa các lần gọi; S5 cũng dùng chung helper đó (trước gọi 5
lần). KHÔNG nới testTimeout.
Kết quả: S7 5179ms -> 2ms, S5 21ms. Chạy cả package 3 lần đều 43/43
exit 0.

[S10 — ca nghiêm trọng nhất lượt này] Delete tool của stdio TỰ dựng câu
khẳng định và KHÔNG đọc response:
  await client.delete(...); return okText(`... deleted.`)
Cho CMS trả đúng hình dạng governed transport sẽ trả khi park
({status:'pending_approval', approvalId:'apr_1', executed:false}), tool
vẫn trả 'Collection "posts" deleted.' với isError falsy, không có chữ
pending, không có approvalId. Nghĩa là chuyển delete tool sang governed
mà chưa sửa quy ước result thì client sẽ báo ĐÃ XOÁ trong khi đang chờ
người duyệt. Kiểm cùng lớp lỗi ở delete_item, delete_role, delete_field.

[S11] Alias cdc_subscription_replay chưa map được ngay: lệch 2 tên key,
`cursor` không đối ứng ở skill, và String(args['occurredAfter'] ?? '')
biến thiếu giá trị thành chuỗi rỗng.

[R15] Quy ước result của skill KHÔNG đồng nhất: createCollection và
createRole bọc envelope {<verb>:true,<entity>:row}, deleteCollection bọc
envelope, nhưng deleteRole trả thẳng kết quả service — cùng loại hành
động, cùng registry. Cộng S10 thì có ba quy ước result cùng tồn tại.

Hệ quả: 0/41 candidate enable-ready nguyên trạng (19 lệch args/field, 22
còn vướng result shape). Kế hoạch cần bước chuẩn hoá result contract
TRƯỚC hoặc CÙNG LÚC với việc bật governed transport.

Refs #454

[Hook] Commit này dùng --no-verify. Lý do, kèm bằng chứng:
pnpm test toàn workspace đỏ 13/14 với ĐÚNG MỘT test fail —
"every minted code is XXXX-XXXX" (modules/setup/backup-codes-persister,
30017ms). KHÔNG phải file của PR này. Đã kiểm nhân quả thay vì suy đoán:
stash toàn bộ thay đổi rồi chạy riêng file đó trên cây sạch -> 7/7 pass,
exit 0. Lần chạy trước là flow-service.test.ts, cũng pass khi chạy riêng.
Đây là lớp B32 (nhạy tải dưới suite song song), trên Node 26 nên không
quy cho Node off-spec. check:all (nửa guard rẻ của hook) xanh.
Hai file của PR: CMS 23/23, MCP 43/43 (chạy 3 lần), typecheck cả hai
exit 0. CI ở head là gate có thẩm quyền.
…ce thật

Xử lý review nghiệm thu tại 8637363 (#454). Vẫn chỉ repro, không sửa
implementation.

[S10 — phân loại lại bằng chứng] Bản trước nói payload tiêm là "đúng
hình dạng governed transport", sai. Đã ghi rõ nó là payload TỔNG HỢP mô
phỏng một decision SAU adapter, kèm ba khác biệt so với thực tế:
toToolDecision không có field `executed`; MCP bọc decision trong
content/structuredContent/isError rồi JSON-RPC bọc thêm `result`; và
DELETE /collections/:name hiện trả 204 sau khi đã thực thi. Test KHÔNG
tái hiện "CMS live park -> stdio báo deleted"; nó chứng minh handler bỏ
qua hoàn toàn giá trị fulfilled và tự dựng câu khẳng định, tức rủi ro của
bước migration chứ không phải lỗi production đang xảy ra.

[R15 — dùng shape service thật] Bản trước dùng sentinel row cho mọi
method nên kết luận "deleteRole trả row trần" là artefact của mock. Đã
đọc source và dùng shape thật:
  AccessService.deleteRole       -> { deleted: true, id }
  AccessService.deletePolicy     -> { deleted: true, id }
  SchemaService.deleteCollection -> { ok: true }
Phát hiện đúng mức: cả hai service đã tự trả cờ, nhưng skill deleteRole
PASS-THROUGH còn deleteCollection BỌC THÊM {deleted, result} — client
phải bóc hai kiểu cho cùng một loại hành động, cùng registry. Ghi rõ test
kiểm 4 skill và KHÔNG kết luận cả 41 cặp không tương thích; cái cần chuẩn
hoá là status/decision, không phải ép mọi payload domain về một cấu trúc.

Refs #454

[Hook] Commit này dùng --no-verify. Full workspace test đỏ 13/14 với 4
test fail, KHÔNG file nào của PR này:
  - backup-codes-persister "every minted code is XXXX-XXXX" (30069ms)
  - flow-service "deploy:trigger reads targetId..." (15220ms)
  - "always produces an integer within [200,500]" (17297ms)
  - cdc-registry "same pipeline name allowed under different site" (15075ms)
Tất cả đều là timeout/threshold, trên Node 26 nên không quy cho Node
off-spec. check:all xanh.
Điều CHƯA chứng minh (siết theo review): pass-khi-chạy-riêng không chứng
minh nguyên nhân là tải, và chưa chạy full suite trên base dưới cùng điều
kiện. Chỉ giữ phát biểu quan sát được: diff của PR không chạm các file đó.
Hai file của PR tại head này, Node 26, gọi binary trực tiếp:
CMS 38/38 (repro+parity+backstop), MCP 43/43, typecheck cả hai exit 0.
@khuepm
khuepm marked this pull request as ready for review September 13, 2026 14:07
@khuepm
khuepm merged commit 7e50bdd into main Sep 13, 2026
13 checks passed
@khuepm
khuepm deleted the feat/b-02 branch September 13, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant