SDD-030 — Nemo12 Tool Plane
Đặc tả chủ dự án 2026-09-02 (SRC-667): dựng Nemo12 Tool Plane — tầng năng lực domain mà AI Agent (Claude Managed Agents) dùng để chạm vào hệ Nemo12. Bản canonical này giữ nguyên các quyết định của đặc tả, ghi thêm chỗ MVP thu hẹp và vì sao.
Câu một dòng của cả tài liệu: Agent nhận NĂNG LỰC Nemo12, không nhận CREDENTIAL Nemo12.
Code: workers/tool-plane/. Mặt tiền: https://mcp.nemo12.com/mcp. Bảng: tp_* (migration 0188).
1. Vị trí trong kiến trúc Conan
Claude Managed Agent ──(suy luận)──┐
│ │
│ MCP │ Sandbox: git clone · sửa code · test · build
▼ ▼
Conan Capability Plane (máy tạm của Agent)
github.* cloudflare.* …
│
▼
Nemo12 Tool Plane ← tài liệu này
learner.* assessment.* learning.* coral.* curriculum.* application.* nemo12.* docs.*
│
├── D1 nemo12-platform (binding trực tiếp)
├── api.nemo12.com (internal API, khi module đó sở hữu dữ liệu)
└── Coral / Foundry (qua api)Hai tầng năng lực phải tách: Conan lo hạ tầng và việc xuyên danh mục (GitHub, Cloudflare, Notion), Nemo12 Tool Plane lo thứ cần hiểu domain Nemo12 (learner, readiness, knowledge). Gộp hai tầng là để logic nghiệp vụ Nemo12 rò vào nền tảng chung — lỗi kiến trúc mà đặc tả §31 cấm.
Ranh giới trách nhiệm (đặc tả §37): Managed Agent = suy luận và tự chạy · Sandbox = máy tạm của Agent · Nemo12 Tool Plane = năng lực domain · Conan Capability Plane = năng lực xuyên danh mục · Cloudflare = hạ tầng · GitHub = nguồn sự thật của code.
2. Nguyên tắc thiết kế
- Năng lực, không phải API. Agent gọi
learner.get_learning_progress, không gọiGET /v1/learners/{id}/progress. Tool là ranh giới trừu tượng: API bên trong đổi thì tool giữ nguyên (§8 versioning). - Tool theo domain, không theo bảng. Cấm lộ
d1.select/d1.insert— trừ khi một việc vận hành nội bộ đòi hỏi rõ ràng, và khi đó phải là tool CRITICAL có duyệt người. - Least privilege. Mỗi Agent chỉ có grant cho tool nó cần; không có grant là không gọi được gì, kể cả
tools/listcũng chỉ hiện tool được cấp (§5.1). - Không credential. Agent không bao giờ cầm D1 credential, Cloudflare token, secret dịch vụ nội bộ hay API key bên thứ ba. Chỉ worker Tool Plane cầm binding.
- Đọc và ghi phân biệt được từ registry. Mỗi tool khai
mode: read | writevàrisk; policy đọc hai trường đó, không cần đọc code executor. - Trả lý do kèm đề xuất. Tool về recommendation trả
reasonđể Agent giải thích được với người dùng (đặc tả §8). - Giữ năm khái niệm đo tách bạch: coverage · mastery · evidence · confidence · readiness. Không tool nào được gộp chúng thành một điểm (đặc tả §9; cùng luật với SDD-002/SDD-008).
3. Mô hình domain
Domain ban đầu: Learner · Parent · Learning · Assessment · Knowledge (Coral) · Curriculum · Learning Package · Mentor · Community · Application · Analytics. Tương lai: Commerce · Subscription · Communication · Events · Competition · Admissions.
MVP (§10) chỉ hiện thực Learner · Assessment · Learning · Knowledge · Application và một tool ghép nemo12.*. Parent/Mentor/Curriculum/Learning Package là registry trống có chủ đích — thêm tool là thêm file trong src/domains/<domain>/ và một dòng seed tp_tools.
4. Registry
4.1 Application Registry — tp_applications
Cột: id · name · domain · repository · repository_path · environment · owner · status · description · architecture_reference · database · deployment_target · metadata (JSON) · created_at · updated_at. Seed 0188: nemo12-web · nemo12-api · nemo12-learn · nemo12-coral · nemo12-foundry · nemo12-tool-plane. repository_path là đường trong monorepo conan1/nemo12.com (đặc tả ví dụ một repo mỗi app; Nemo12 là monorepo nên giữ cả hai cột). Agent suy luận về hệ Nemo12 từ bảng này, không hardcode.
4.2 Tool Registry — code là nguồn sự thật
workers/tool-plane/src/registry/tools.ts gom ToolDefinition từ các domain: name · version · description · category · risk · mode · input (zod) · learnerIdField · execute. Bảng tp_tools là bản chiếu để grant/policy tham chiếu và người vận hành đọc; khớp lại bằng node scripts/tool-plane/sync-tools.mjs (in SQL, dán vào migration — không tự chạy lên production, theo luật deploy qua GitHub). Chọn code làm nguồn vì schema zod vừa validate vừa sinh JSON Schema cho tools/list; giữ hai bản schema là có ngày lệch.
4.3 Agent Registry — tp_agents + tp_agent_tools
tp_agents: id · name · description · status · token_hash. Token chỉ lưu SHA-256 (cùng luật session của api). token_hash NULL = chưa cấp, không đăng nhập được. Cấp bằng node scripts/tool-plane/issue-agent-token.mjs <agent_id> — in token MỘT lần + SQL cập nhật hash.
tp_agent_tools (grant): agent_id · tool_id · resource_scope · environment_scope · permission. tool_id nhận id có version, tên trần, hoặc glob learner.*. Seed 0188 theo đặc tả §3.3:
| Agent | Tool | resource_scope |
|---|---|---|
learning-agent | learner.* (4 tool đọc) · assessment.* · learning.* · nemo12.get_learner_learning_state | user |
learning-agent | coral.search_knowledge · coral.get_knowledge_node | * |
research-agent | coral.search_knowledge · coral.get_knowledge_node | * |
engineering-agent | application.list · application.get · application.get_repository · application.get_environment | * |
| cả ba agent | docs.search_docs · docs.get_doc (seed scripts/tool-plane/seed-docs-tools.sql, SRC-1322) | * |
Không Agent nào có grant write. learner.update_profile tồn tại trong registry đúng để bài test thứ ba (§11) chứng minh cổng chặn được.
5. Giao diện MCP và Tool Router
5.1 MCP — POST https://mcp.nemo12.com/mcp
Streamable HTTP, JSON-RPC 2.0, tự viết (không kéo SDK) vì bề mặt MVP chỉ có initialize · ping · tools/list · tools/call; không SSE, không session phía server — mỗi request tự đủ, đúng kiểu Worker. tools/list lọc theo grant của Agent đang gọi. Lỗi của tool trả trong result với isError: true và structuredContent là envelope §8; lỗi JSON-RPC chỉ dành cho request sai hình. Nâng lên SDK khi cần resources/prompts.
Có route công khai là cố ý: Managed Agent chạy ở Anthropic, không ở Cloudflare, nên không có service binding nào tới được nó (khác nemo12-foundry). Đổi lại, không token là 401 trước khi chạm bất kỳ bảng nào; /health là đường duy nhất không cần auth.
5.2 Tool Router — src/router.ts
identify tool → validate schema → load grant + policy → evaluatePolicy (§7.1)
→ resource-level authz (§7.2, nếu tool có learner_id) → execute → audit (§7.4)Router không chứa logic domain; nó chỉ biết "một tool" là gì. Mọi nhánh (kể cả tool không tồn tại, input sai, bị từ chối) đều đi qua audit — sổ chỉ ghi lượt thành công thì không thấy ai đang dò quyền.
6. Xác thực
Mỗi request mang Authorization: Bearer <token>. Token là danh tính, không phải quyền: có token chỉ nghĩa là "tôi là learning-agent". DB chỉ giữ SHA-256, ở hai chỗ:
| Bảng | Khi nào | user_id |
|---|---|---|
tp_agent_tokens (0189) | đường chính: một agent nhiều token, mỗi token có thể trói vào một user | từ token |
tp_agents.token_hash (0188) | token không trói user, đường cũ | từ header nếu có |
Vì sao user nằm trong token: Claude Managed Agents chỉ gửi được Authorization tới MCP server, không gửi được header tuỳ ý (tài liệu MCP connector của Managed Agents). Nên "Agent thay mặt ai" phải nằm trong chính token; mỗi vault của Managed Agents giữ một token, và vault vốn là một người dùng, nên mô hình khớp. Token đã trói thì header X-Nemo12-User-Id bị bỏ qua — token quyết, không phải header, để không ai mượn token mà đổi người.
Ba header ngữ cảnh đều tuỳ chọn: X-Nemo12-Session-Id (thiếu thì lấy Mcp-Session-Id, thiếu nữa thì cấp S-<uuid>), X-Nemo12-User-Id (chỉ nhận khi token không trói), X-Request-Id (thiếu thì cấp REQ-<uuid>, trả lại trong header). Ra Principal { agent_id, session_id, user_id, request_id } — bốn định danh mà đặc tả §19 đòi.
Cấp token: node scripts/tool-plane/issue-agent-token.mjs <agent_id> [--user <user_id>] in token một lần + SQL nạp hash. Nối vào Managed Agents: scripts/tool-plane/managed-agents-setup.mjs tạo environment, vault (static_bearer cho mcp.nemo12.com/mcp) và agent cho cả ba agent; cần ANTHROPIC_API_KEY trong môi trường lúc chạy.
7. Ủy quyền, rủi ro, duyệt người, audit
7.1 Policy Engine — hàm thuần (src/policy/policy-engine.ts)
Thứ tự luật, chặt thắng dễ:
- Không có grant cho tool → deny
- Grant không phủ
environmenthiện tại (TOOL_PLANE_ENVIRONMENT) → deny - Tool
writemà grantread→ deny - Policy
denykhớp → deny - Policy
require_approvalkhớp → require_approval - Tool
CRITICAL→ require_approval (luật cứng, không policy nào bỏ được) - Còn lại → allow
Policy (tp_policies): effect · agent_scope · tool_scope · resource_scope · environment_scope · conditions với glob *, tiền tố learner.update_*, danh sách phẩy, và conditions JSON (risk_at_least, mode). Seed 0188: deny-learner-writes (chặn learner.update_* cho mọi agent) · approve-high-risk-writes · approve-production-writes. Conditions hỏng JSON làm policy không khớp — được ghi rõ trong test, vì một policy deny hỏng mà "tự thành allow" là điều phải biết chứ không phải điều bất ngờ.
Bốn cấp của đặc tả §20 ánh xạ: Agent level = bước 1 · Environment level = bước 2 · User/Resource level = §7.2 · Policy = bước 4-6.
7.2 Resource level — learner cụ thể (src/policy/authorization.ts)
Biết learner_id không phải là có quyền (đặc tả §13). resource_scope của grant quyết:
| scope | nghĩa |
|---|---|
* | mọi learner — chỉ cho agent nội bộ (aggregate/research) |
user | learner mà user đang thay mặt được vào: self → family_members (owner/guardian/supporter) → guardians → admin/staff → mentor chỉ learner đã phân công (mentor_assignments.status='active') |
learner:L-1,learner:L-2 | danh sách cứng |
family:F-1 | mọi learner trong gia đình |
Khác learnerAccess() của api ở một điểm có chủ ý: mentor qua Tool Plane chỉ được learner đã phân công (đặc tả §14), trong khi api cho mentor xem mọi learner (SRC-037). Agent thay mặt mentor là bề mặt mới, thu hẹp trước rồi nới sau khi có nhu cầu thật.
Learner không tồn tại trả AUTHORIZATION_ERROR chứ không RESOURCE_NOT_FOUND: nói "không có learner này" là xác nhận id nào tồn tại — một kênh dò id.
7.3 Mức rủi ro và duyệt người
LOW (mọi tool đọc MVP) · MEDIUM (learning.create_learning_plan, chưa có) · HIGH (learner.update_profile) · CRITICAL (xoá dữ liệu production, chưa có). Luồng duyệt người (đặc tả §22) trong MVP dừng ở APPROVAL_REQUIRED + audit: chưa có hàng chờ duyệt. Không có nhánh "tạm cho qua" — thiếu người duyệt là dừng. Hàng chờ duyệt là việc kế tiếp khi có tool ghi đầu tiên.
7.4 Audit — tp_audit_logs
Một dòng mỗi lượt gọi: request_id · timestamp · agent_id · session_id · user_id · tool_id · resource · decision · status · error_code · duration_ms · environment · metadata. Không ghi input/output — chỉ định danh tài nguyên (learner:L-1) và mã lỗi, nên sổ không chứa dữ liệu learner (đặc tả §23). Ghi sổ hỏng thì console.error kèm cả event và nghiệp vụ vẫn chạy (QG-015).
8. Mô hình lỗi và versioning
Envelope duy nhất: { success: false, error: { code, message, retryable, details? } } với mười mã AUTHENTICATION_ERROR · AUTHORIZATION_ERROR · RESOURCE_NOT_FOUND · VALIDATION_ERROR · CONFLICT · RATE_LIMITED · DEPENDENCY_ERROR · INTERNAL_ERROR · POLICY_DENIED · APPROVAL_REQUIRED. retryable mặc định theo mã; details không bao giờ chứa dữ liệu learner. Lỗi không phải ToolError thành INTERNAL_ERROR, thông điệp gốc chỉ vào log.
Tool có version: id đầy đủ learner.get_profile:v1; Agent gọi theo tên trần được bản mới nhất, gọi theo id được đúng bản đó. Đổi hình dạng output là tăng version và giữ bản cũ tới khi không còn Agent nào gọi (đọc tp_audit_logs.tool_id).
9. Chiến lược truy cập dữ liệu và tool ghép
Ưu tiên (đặc tả §26): domain service → D1 binding trực tiếp → internal API. MVP dùng binding D1 trực tiếp vào nemo12-platform cho mọi tool đọc, vì các engine (readiness, learning plan) đã ghi bản model vào learner_model_versions — Tool Plane đọc bản đã tính, không tính lại. Tool nào cần chạy engine (ví dụ tạo plan) sẽ đi qua api, không nhân đôi thuật toán ở đây. Ngưỡng stateFor (solid ≥ 0.8 · shaky ≥ 0.5 · gap · unknown khi confidence < 0.25) sao từ api và ghi rõ là bản sao — đổi ở api thì đổi ở đây.
Tool ghép (§27): nemo12.get_learner_learning_state = profile + context + progress + readiness + next. Agent nhận một bức tranh, không phải xâu năm tool và không phải biết Nemo12 cất mỗi mảnh ở đâu.
9.1 Domain docs: tra tài liệu canonical (SRC-1322)
Agent cần đọc docs (SDD, PRD, luật vận hành) mà không tải cả trang 50 KB vào ngữ cảnh. Hai tool đọc, LOW risk, không chạm dữ liệu learner:
| Tool | Input | Trả về |
|---|---|---|
docs.search_docs | query, limit (1-10, mặc định 5) | mỗi hit: path · title · heading_path · url trên docs.nemo12.com · score · snippet khoảng 800 ký tự quanh chỗ khớp |
docs.get_doc | path (có hoặc không .md, chấp nhận tiền tố docs/ hay URL docs.nemo12.com), section tuỳ chọn | cả trang markdown kèm danh sách mục H2, hoặc chỉ một mục H2 khi có section |
Thiết kế chọn cái rẻ nhất mà vẫn chắc:
- Nguồn là
docs/**/*.mdtrong repo, quét bằng glob trongscripts/gen-docs-index.mjs(không liệt kê đường dẫn nào, vì trang đang được tách thư mục liên tục). Mỗi trang cắt theo H2 (bỏ qua H2 nằm trong khối code), frontmatter bỏ đi, tiêu đề lấy từtitle:. - Chỉ mục là file dẫn xuất, không commit:
docs-index.txttrong thư mụcgenerated/của worker (git-ignored, chỉ có sau khi sinh), sinh bằngnpm run gen:docs-index..github/app-paths.jsonkhaipregenchotool_planevà thêmdocs/vàopaths, nên sửa docs nào cũng kéo theo deploy lại Tool Plane với chỉ mục mới. Wrangler đóng gói file như Text module (khoảng 2.8 MB gzip, dưới trần 10 MB của gói Paid). - Không D1, không KV, không embeddings: Tool Plane chưa dùng Workers AI, và câu hỏi "trang nào nói về X" đủ với BM25 trên từ khoá. Token hoá bỏ dấu tiếng Việt (
xac thuckhớpxác thực); tiêu đề trang và tiêu đề mục được tính nặng hơn thân bài. - Dựng lười: JSON chỉ parse và lập chỉ mục ngược ở lần gọi docs đầu tiên của một isolate (khoảng 0.4 s, khoảng 50 MB heap đo trên toàn bộ docs/ ngày 10.10.2026); các tool khác không trả giá gì.
- Trang mới hơn chỉ mục:
get_docthử bản.mdtrênhttps://docs.nemo12.com/<path>.md; không có thìRESOURCE_NOT_FOUND, mạng lỗi thìDEPENDENCY_ERROR(retryable).
Quyền: grant thường như mọi tool. scripts/tool-plane/seed-docs-tools.sql (nạp qua seed-data.yml sau khi worker đã deploy) ghi hai dòng tp_tools và cấp cho cả ba agent; chưa nạp thì tool có trong code nhưng không agent nào thấy. Test hành vi: workers/tool-plane/src/domains/docs/docs.test.ts chạy qua đúng mặt MCP với chỉ mục mẫu.
10. MVP — những gì có ở bản 0.1
Hạ tầng: Worker nemo12-tool-plane · MCP server · D1 chung · route mcp.nemo12.com · CI deploy theo scope (app_tool_plane) + deploy-app.yml (app tool_plane) chạy tay.
| Domain | Tool | risk/mode |
|---|---|---|
| learner | get_profile · get_context · get_learning_progress · get_competency_profile | LOW/read |
| learner | update_profile | HIGH/write — bị policy chặn |
| assessment | get_readiness · get_mastery | LOW/read |
| learning | get_current_plan · get_next_learning_experience | LOW/read |
| coral | search_knowledge · get_knowledge_node | LOW/read |
| application | list · get · get_repository · get_environment | LOW/read |
| nemo12 | get_learner_learning_state | LOW/read |
| docs | search_docs · get_doc (SRC-1322, §9.1) | LOW/read |
Hồ sơ learner cố ý bỏ birth_date, facebook_url, links_json, ảnh: Agent cần tuổi và lớp để dạy, không cần ngày sinh và mạng xã hội của một đứa trẻ.
11. Ba bài test đầu-cuối (đặc tả §35)
Đã hiện thực trong workers/tool-plane/src/router.test.ts trên D1 giả:
learning-agentthay mặt phụ huynh gọinemo12.get_learner_learning_statecho con → allow, kết quả có cấu trúc, auditallow/success/learner:L-1. Đảo chiều: learner của gia đình khác →AUTHORIZATION_ERROR, learner không tồn tại → cùng mã.coral.search_knowledge("quadratic equations")→ node có cấu trúc.learner.update_profilevới grant write có sẵn →POLICY_DENIEDbởideny-learner-writes, không chạm bảnglearners, auditdeny/denied/POLICY_DENIED.
Bài trên production (chưa chạy, cần token): cấp token cho learning-agent, gọi tools/list rồi tools/call bằng curl in ra từ script cấp token.
12. Tiến hoá
Không dùng Claude Managed Agents (chỉ đạo chủ dự án 2026-09-02). Agent chạy tự động (gợi ý học hằng ngày, báo cáo phụ huynh) là việc của Cloudflare Workflows trong workers/, gọi Tool Plane bằng token của agent như mọi MCP client khác; không có tài khoản trả theo token ở Anthropic cho việc này. scripts/tool-plane/managed-agents-setup.mjs giữ lại làm tài liệu về cách nối một MCP client ngoài, không nằm trong đường vận hành.
Thêm domain = thêm src/domains/<domain>/index.ts + seed tp_tools + grant. Tool ghi đầu tiên kéo theo hàng chờ duyệt người (§7.3). Khi tool cần chạy engine thì gọi api qua service binding API (chưa khai — thêm vào wrangler.jsonc và env.ts khi có tool đầu tiên cần). Mục tiêu không phải lộ mọi endpoint, mà là lộ đúng năng lực một Agent thông minh thật sự cần.
13. Trace
| Mục | Yêu cầu | Cổng |
|---|---|---|
| §2 nguyên tắc · §4 registry · §5 MCP + router · §8 lỗi/versioning | REQ-PLT-22 | QG-004 |
| §6 xác thực · §7 ủy quyền, policy, audit | REQ-PLT-22, REQ-SEC-02 | QG-008 |
| §9 dữ liệu chỉ qua Tool Plane, Agent không cầm credential | REQ-PLT-22, REQ-PLT-09 | QG-010 |
| §11 ba bài test | REQ-PLT-22 | QG-004 |