# Điều khiển plugin bằng MCP

> The agent in ai-chat calls tools of plugins attached to the session; tools are filtered per plugin and dangerous actions always prompt.

# Plugin MCP Control

## Mục đích

Cho phép AI agent chạy bên trong plugin `ai-chat` (Claude Agent SDK, cùng tiến trình Electron)
list/toggle/đọc-ghi CÁC PLUGIN KHÁC qua MCP tool — giống hệt những gì `mcp-server/` (dev tool ngoài,
chạy qua HTTP bridge) đã làm cho Claude Code, nhưng chạy trực tiếp trong-process (không cần bridge).
Mỗi plugin backend mới cũng có thể tự khai báo thêm tool riêng qua `getMcpTools(ctx)`, được `ai-chat`
tự động gom vào.

> **CẬP NHẬT (per-session attachment):** Dev Mode ĐÃ BỎ khỏi cơ chế này. Tool KHÔNG còn "tất cả
> hoặc không" theo Dev Mode nữa — mỗi session tự chọn plugin nào được gán (kể cả nhóm 8 tool điều
> khiển). Xem mục "Per-Session Plugin Attachment" bên dưới.

## File liên quan

**Host (`electron/`) — thêm hàm mới, KHÔNG đổi hành vi IPC handler cũ:**
- `electron/plugin-ipc.ts` — tách thân các `ipcMain.handle` cũ (`list-installed`, `get/set-settings`,
  `toggle`) thành hàm export riêng (`listInstalledPlugins`, `getPluginSettingsFor`,
  `setPluginSettingsFor`, `togglePluginEnabled`) — handler cũ gọi lại y hệt, hành vi không đổi. Thêm
  MỚI `getPluginManifestFor`, `readInstalledPluginSource`, `writeInstalledPluginSource` (đọc/ghi file
  plugin đã cài, chặn path traversal — port từ `mcp-server/src/tools/plugin-manager.ts`).
- `electron/plugin-loader.ts` — thêm export `getLibraryPluginPath()` (trước đó là hàm nội bộ
  `getLibraryPath()`), dùng cho fallback đọc manifest/source khi plugin chưa cài.
- `electron/plugin-host.ts` — import các hàm trên từ `plugin-ipc.ts` (lưu ý: tạo dependency vòng
  giữa 2 file, AN TOÀN vì cả 2 chiều chỉ dùng function bên trong thân hàm khác, không dùng ở top-level
  module — pattern CommonJS phổ biến). Build `ctx.modules.pluginControl` (gom các hàm trên) trong
  `buildContext()`. Thêm `collectPluginMcpTools()` — quét registry `loaded` (Map plugin đang active),
  gọi `module.getMcpTools?.(ctx)` của từng plugin, gộp lại + tự thêm tiền tố `<pluginId>_` vào tên tool
  để tránh trùng. Thêm ring buffer `pluginLogs` (Map<pluginId, entry[]>, tối đa 300 dòng/plugin) —
  `ctx.log()` (đã có sẵn) giờ ghi thêm vào buffer này song song với việc broadcast tới renderer; expose
  qua `getPluginLogsFor(pluginId, limit?)`.
- `plugins-library/_shared/plugin-host-types.d.ts` — thêm `modules.pluginControl` vào `PluginContext`
  + optional `getMcpTools?(ctx): any[]` vào `PluginMainModule` (chỉ thêm type, không đổi runtime).

**ai-chat (`plugins-library/ai-chat/`):**
- `main/plugin-control-mcp.ts` (file mới) — `buildPluginControlMcpServers(ctx)`: trả `undefined` nếu
  `ctx.store.get('devMode')` false; ngược lại dùng `ctx.modules.claudeAgent.createSdkMcpServer`/`.tool`
  (in-process MCP server, có sẵn từ SDK 0.3.201, không cần subprocess/stdio) tạo server `kazuo-plugins`
  gồm **8 tool chung** (`list_plugins`, `get_plugin_manifest`, `toggle_plugin`, `get_plugin_settings`,
  `set_plugin_settings`, `read_plugin_source`, `write_plugin_source`, `get_plugin_logs` — wrap
  `ctx.modules.pluginControl.*`) + tool riêng từ `collectPluginMcpTools()`. Mỗi tool handler bọc
  try/catch trả `isError: true` thay vì throw (pattern giống `mcp-server`'s dev tool).
- `main/agent-session.ts` — `AgentStartConfig` thêm field optional `mcpServers?: Record<string, any>`;
  `start()` chỉ set `options.mcpServers` nếu có giá trị (no-op khi absent, không đổi hành vi cũ).
- `main/chat-manager.ts` — `connectAgentFor()` gọi `buildPluginControlMcpServers(ctx)` trước khi
  `t.agent.start(...)`, truyền qua field `mcpServers` ở trên.
- `package.json` — thêm dependency `zod` (input schema cho `sdk.tool(...)`).

**Skill scaffold:**
- `.claude/skills/kazuo-create-plugin/scripts/scaffold-plugin.js` — `type=backend` sinh thêm ví dụ
  `getMcpTools(ctx)` mẫu (tool `ping`) trong `main/index.ts`.
- `.claude/skills/kazuo-create-plugin/references/plugin-runtime-api.md` — mục 2b giải thích quy ước.

## Cách hoạt động

1. Khi 1 tab trong `ai-chat` kết nối agent (`connectAgentFor`), gọi
   `buildPluginControlMcpServers(ctx, t.attachedPlugins)` — trả `undefined` nếu session chưa gán
   plugin nào (`undefined`/`[]`). Dev Mode KHÔNG còn được kiểm tra.
2. Nếu có gán: gói `mcpServers: { 'kazuo-plugins': <in-process McpServer> }` được truyền vào `query()`'s
   options — nhưng CHỈ chứa tool của plugin nằm trong `attachedPlugins` (lọc bởi `attach-filter.ts`).
   8 tool điều khiển chỉ có mặt khi `__plugin-control__` được gán. Tên tool business vẫn là
   `<pluginId>_<tên ngắn>`; tên 8 tool control giữ nguyên (không prefix).
3. Mọi tool đều gọi thẳng hàm trong `electron/plugin-ipc.ts` (qua `ctx.modules.pluginControl`) — CÙNG
   code path Plugin Manager UI dùng, nên toggle/set-settings qua AI cũng broadcast live-update tới
   widget đang mở y hệt thao tác tay.
4. `read_plugin_source`/`write_plugin_source` đọc/ghi trực tiếp file trong
   `userData/plugins/<id>/...` (write chỉ áp dụng plugin ĐÃ CÀI, không ghi được vào
   `plugins-library/` gốc) — có chặn path traversal.
5. `get_plugin_logs` đọc ring buffer main-process (KHÔNG phải `window.__dwPluginLogs` phía renderer —
   2 buffer riêng biệt, cùng nguồn dữ liệu là `ctx.log()` nhưng lưu ở 2 nơi khác nhau). Chỉ bắt được
   message đi qua `ctx.log()`; `console.log` trực tiếp trong `main/index.ts` không được ghi lại.

## Giới hạn / rủi ro đã biết

- **Thay đổi gán plugin không áp dụng giữa lượt** — tools chỉ được tính lại khi tab kết nối lại agent
  (giống việc đổi model/system prompt). `setAttachedPlugins` dispose agent để lần gửi sau kết nối lại.
- Dependency vòng `plugin-ipc.ts` ↔ `plugin-host.ts`: đã biên dịch sạch qua `tsc -p
  tsconfig.node.json --noEmit` sau khi thêm; nếu sau này thêm code chạy Ở TOP-LEVEL module (không
  phải trong thân hàm) tham chiếu 2 chiều, cần kiểm tra lại thứ tự load.
- `write_plugin_source` cho AI quyền ghi đè file code plugin khác đang chạy thật. Sau khi bỏ Dev Mode,
  2 lớp bảo vệ: (1) người dùng phải TỰ TAY gán nhóm `__plugin-control__` vào session; (2) **`write_plugin_source`
  LUÔN bật prompt xác nhận Allow/Deny** (kể cả ở `bypassPermissions` mode) — xem `requestConfirm` /
  `makeConfirmRequest` / `pendingConfirms` (tái dùng event `permission_request` + IPC `permission` sẵn có).
  7 tool control còn lại (list/toggle/settings/read/logs...) vẫn chạy thẳng khi đã gán.
- Đã biên dịch kiểm tra: `tsc -p tsconfig.node.json --noEmit` (host) và `tsc -p tsconfig.plugins.json`
  (ai-chat + mọi plugin backend, kể cả plugin test có `getMcpTools` scaffold) — cả hai pass sau mỗi
  bước sửa.
- Chưa test runtime thật trong Electron (chỉ verify qua tsc + unit test) — cần chạy `npm run dev`,
  mở ai-chat, gán plugin cho session, thử hỏi AI "liệt kê tool bạn có" để xác nhận lọc đúng.

## Per-Session Plugin Attachment

Mỗi session (tab) của `ai-chat` tự chọn plugin nào được gán; chỉ tool của plugin được gán mới lọt vào
agent của session đó. Gán được cho nhiều session độc lập, lưu bền theo session. **Dev Mode vô can.**

### File liên quan (chỉ `plugins-library/ai-chat/`, KHÔNG đụng host)

- `main/attach-filter.ts` (MỚI) — logic thuần, không phụ thuộc ctx/SDK/Dev Mode:
  - `CONTROL_PLUGIN_ID = '__plugin-control__'` — pseudo-plugin cho 8 tool điều khiển.
  - `pluginIdOf(toolName, knownIds)` — match prefix DÀI NHẤT (id có dấu `-` như `database-manager`
    không bị nhầm với `database`).
  - `filterMcpTools(allTools, attachedPlugins, knownIds)` — `undefined`→tất cả (no-filter), `[]`→rỗng,
    `[...]`→lọc theo owner.
  - `groupAttachablePlugins(allTools, installed, {controlToolCount})` — gom tool theo plugin cho picker,
    LUÔN kèm entry control.
- `test/attach-filter.test.js` (MỚI) — unit test `node:test` (chạy `node --test` từ thư mục ai-chat,
  sau khi `tsc -p tsconfig.plugins.json`). Nằm ngoài `main/**` nên KHÔNG bị compile/ship.
- `main/plugin-control-mcp.ts` — `buildPluginControlMcpServers(ctx, attachedPlugins?)`: bỏ gate Dev Mode;
  8 tool control giữ tên gốc (unprefixed), chỉ gồm khi `__plugin-control__` được gán; business tool lọc
  qua `filterMcpTools`; trả `undefined` khi rỗng. Thêm `listAttachablePlugins(ctx, attachedPlugins)` —
  plugin đang bật có tool + control + plugin đã gán mà đang tắt (`enabled:false`, hiện mờ trong picker).
- `main/chat-manager.ts` — `TabState.attachedPlugins` + `SessionMeta.attachedPlugins`;
  `saveSessionSnapshot` lưu; `workspaces()` trả kèm; `connectAgentFor` truyền `unionPlugins(t.attachedPlugins,
  workspacePlugins[cwd])` (session + workspace). Có `setSessionAttachedPlugins(sessionId, list)`,
  `attachableForSession(sessionId, cwd)`, `setWorkspaceAttachedPlugins(cwd, list)`,
  `attachableForWorkspace(cwd)` — xem mục "Hai cấp" bên dưới. **Không còn** `setAttachedPlugins(tabId, ...)`
  / `attachablePlugins(tabId)` kiểu tab-based (đã thay bằng 4 hàm session/workspace ở Phase 5).
- `main/index.ts` — 4 IPC trong `CHANNELS`: `set-session-plugins`, `attachable-for-session`,
  `set-workspace-plugins`, `attachable-for-workspace` (không còn `set-attached-plugins`/`attachable-plugins`).
- `widget.html` + `js/widget.js` + `css/widget.css` — **picker nổi mở từ context menu chuột phải**
  ở sidebar (KHÔNG còn nút subbar). Chuột phải session → "🧩 Gán plugin…"; chuột phải workspace →
  "🧩 Gán plugin cho thư mục…". `createTab`/`tabCfg`/`openTabForSession` vẫn mang `attachedPlugins`.

### Hai cấp: session + workspace (kế thừa union)

- **Session-level:** `sessionMeta[sessionId].attachedPlugins` (như trên).
- **Workspace-level:** setting `workspacePlugins: { [cwdLower]: string[] }` — mặc định của thư mục.
  Đặt qua chuột phải workspace.
- **Effective (lúc connect):** `unionPlugins(session.attachedPlugins, workspacePlugins[cwd])` — session
  tự kế thừa plugin của thư mục khi (re)connect ("khởi động lại tự nhận").
- **Picker session:** plugin do workspace cấp hiện **tick mờ + khóa** (`fromWorkspace`), chỉ sửa được
  phần riêng của session.
- **Warning ⚠️** trên dòng session ở sidebar khi `session.attachedPlugins ∩ workspacePlugins[cwd] ≠ ∅`
  (session tự gán trùng plugin thư mục đã cấp) — dùng `overlapPlugins`.
- Backend: `setSessionAttachedPlugins` / `attachableForSession` / `setWorkspaceAttachedPlugins` /
  `attachableForWorkspace`; IPC `set-session-plugins`, `attachable-for-session`, `set-workspace-plugins`,
  `attachable-for-workspace`. `setWorkspacePlugins` dispose agent mọi tab cùng cwd để reconnect nhận union.

### Semantics

- `attachedPlugins` là mảng pluginId (dùng `__plugin-control__` cho nhóm control). `undefined`/`[]` →
  không tool từ cấp đó (backend coalesce `?? []`). Không có khái niệm "legacy full".
- **Điều kiện 1 tool chạy** = plugin (session hoặc workspace) được gán AND plugin đang BẬT. Plugin tắt
  mà đã gán → inert cho tới khi bật lại.
- Đổi gán (session hoặc workspace) → dispose agent → áp dụng ở lần gửi kế tiếp.
- Chat MỚI chưa gửi tin (chưa có sessionId) chưa có picker riêng — dựa plugin workspace kế thừa.
