---
url: https://docs.nemo12.com/architecture/sdd-030-tool-plane.md
description: >-
  Bản nháp Tool Plane (SDD-030): tầng năng lực Nemo12 cho AI Agent qua MCP,
  nguyên tắc thiết kế và mô hình domain.
---

# 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

```text
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ế

1. **Năng lực, không phải API.** Agent gọi `learner.get_learning_progress`, không gọi
   `GET /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).
2. **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.
3. **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/list` cũng chỉ hiện tool được cấp (§5.1).
4. **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.
5. **Đọc và ghi phân biệt được từ registry.** Mỗi tool khai `mode: read | write` và `risk`; policy
   đọc hai trường đó, không cần đọc code executor.
6. **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).
7. **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`

```text
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ễ:

1. Không có grant cho tool → **deny**
2. Grant không phủ `environment` hiện tại (`TOOL_PLANE_ENVIRONMENT`) → deny
3. Tool `write` mà grant `read` → deny
4. Policy `deny` khớp → deny
5. Policy `require_approval` khớp → **require_approval**
6. Tool `CRITICAL` → require_approval (luật cứng, không policy nào bỏ được)
7. 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/**/*.md` trong repo**, quét bằng glob trong `scripts/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.txt` trong thư mục `generated/` của
  worker (git-ignored, chỉ có sau khi sinh), sinh bằng `npm run gen:docs-index`. `.github/app-paths.json` khai `pregen` cho
  `tool_plane` và thêm `docs/` vào `paths`, 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 thuc` khớp `xá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_doc` thử bản `.md` trên `https://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ả:

1. `learning-agent` thay mặt phụ huynh gọi `nemo12.get_learner_learning_state` cho con → allow,
   kết quả có cấu trúc, audit `allow/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ã.
2. `coral.search_knowledge("quadratic equations")` → node có cấu trúc.
3. `learner.update_profile` với grant write có sẵn → `POLICY_DENIED` bởi `deny-learner-writes`,
   **không chạm** bảng `learners`, audit `deny/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 |
