feat(page-ai): add skill context and agent profile policy

This commit is contained in:
lix-2026
2026-05-29 21:57:29 +08:00
parent 631205ba2f
commit 49a0545148
18 changed files with 3478 additions and 259 deletions
@@ -0,0 +1,456 @@
# 7-40 Page AI MNote skill library and context tool contract v1
> 创建时间:2026-05-29
>
> 状态:`PROCESS`
>
> OwnerPage AI skill/tool capability surface + MNote host-side context provider
>
> 上位依据:
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
> - `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`
> - `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
> - `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
>
> 参考代码:
> - `reference-code/PilotDeck/src/context/prompt/PromptAssembler.ts`
> - `reference-code/PilotDeck/src/context/extension/ExtensionResolver.ts`
> - `reference-code/PilotDeck/src/tool/builtin/readSkill.ts`
> - `reference-code/PilotDeck/src/context/input/InputProcessor.ts`
>
> 关键口径:Hermes 和 Reasonix 本身已经是完整 agent,拥有自己的记忆、会话隔离、agent loop、工具调度和运行时状态。MNote 不建设第三套 agent runtime,不把 MNote 上下文、工具说明或页面正文强行注入每条用户 prompt。MNote 要提供一个专用 skill library 和结构化 context tools,让 agent 在需要时自行选择读取。
## 1. 背景与问题
`7-39` 已经完成第一轮 Page AI UI 收口:
- `sidebar-page-ai-runtime.js` 已有 `PAGE_AI_AGENT_REGISTRY`
- `PAGE_AI_CONTEXT_REF_REGISTRY` 已包含 `current_page / selection / active_editor / file / folder / changed_files`
- 发送 `/api/hermes/client/runs` 时 payload 已包含 `agentId``contextRefs``allowedRoots``editorTarget``runTargetSnapshot`
- SQLite directory grants 和 user preferences 已作为授权与偏好控制面。
但当前暴露出一个更关键的系统问题:
1. 简单聊天请求,例如“收到请回复收到”,仍可能触发大量 `mnote_doc_fetch` 等工具调用。
2. 当前实现倾向把 MNote page context、tool guidance、runTargetSnapshot 等内容拼成大段 instructions 注入上游 prompt。
3. 这种方式会诱导 agent 把所有请求都理解成 MNote 文档任务,也会增加 token、工具误调用和失败噪音。
4. UI 上 contextRefs 与授权区域已经可勾选,但后台语义仍混杂:勾选范围被当成“必须塞给模型的上下文”,而不是“允许 agent 按需读取的能力边界”。
因此本稿替代原先以 `AgentRunEnvelope` 为中心的方案。`AgentRunEnvelope` 仍保留为 audit / permission / receipt 的宿主侧结构,但不再是默认 prompt 注入中心。
## 2. PilotDeck 对照结论
### 2.1 可借鉴模型
PilotDeck 的 `PromptAssembler` 采用分层上下文:
- `ExtensionResolver.listSkills()` 只返回 skill 名称、描述和 namespace。
- `formatSkills()` 只生成 `<available-skills>` 摘要,并明确提示 `Use the read_skill tool to load the full content of any skill listed below.`
- skill 正文不默认进入每轮 prompt。
- slash command 由 `InputProcessor` 识别后作为用户意图进入 agent loop,不把所有 command body 默认注入。
- MCP instructions 也是单独 `<mcp-instructions>` 块,不与用户原文混写。
这些模型适合 MNote
- Page AI 只暴露“MNote 有哪些能力”。
- 详细技能说明通过 `mnote_skill.read` 或 agent 原生 skill 读取。
- 当前页、选区、文件夹、changed files 通过 context tools 按需读取。
- 用户 prompt 保持干净,避免把普通聊天污染成文档任务。
### 2.2 只作参考实现
PilotDeck 的完整 RouterRuntime、Model Canonical Protocol、AgentLoop、MemoryResolver 和 CompactionEngine 不直接移植。MNote 当前已有 Hermes / Reasonix ACP runtime,不应复制第三套 agent runtime。
可参考但不直接搬运:
- `ExtensionResolver` 的只读 contribution snapshot。
- `read_skill` 懒加载模式。
- tool result budget / tool_result_reference 思路。
- permission runtime 的统一决策链。
### 2.3 不适合 MNote 的内容
- 不做 PilotDeck 式全局模型 router。
- 不做 PilotDeck 式后台 always-on agent runtime。
- 不做独立于 SQLite control-plane 的另一套权限真相。
- 不用定时轮询来刷新 Page AI、文件树或页面树。
- 不把 PilotDeck 的项目 memory dataDir 作为 MNote 默认记忆存储;MNote 若后续做记忆,应按 `workspace_id + user_id` 落 SQLite control-plane 或明确的本地数据目录。
## 3. 第一结论
本阶段目标从“把 envelope 注入得更干净”改为:
1. 建立 MNote 专用 skill library 合同。
2. 建立 MNote context tools 合同。
3. Page AI 发送时保留用户原文,不把 MNote context/tool guidance 强行拼到 prompt_blocks。
4. `contextRefs` 只作为用户授权的上下文范围,agent 需要时自行调用工具读取。
5. `AgentRunEnvelope` 只作为宿主侧 audit / permission / tool input snapshot,不默认进入 prompt。
6. Chat-only agent 不挂载 MNote context toolsHermes / Reasonix 是否暴露 MNote tools 由用户勾选的 `contextRefs` 与授权边界决定,普通消息是否调用工具由 agent 自己判断。
7. UI 继续按 7-39 的 Cline 式模型收敛:输入区只保留 `+ / Agent / 上下文 / 停止 / 发送`,不显示 `write · /path` 黑色授权提示。
## 4. 产品设计:Page AI 输入区
### 4.1 默认输入区
Page AI 输入区默认只显示:
```text
[+] [Agent] [上下文] [停止] [发送]
```
要求:
- `Agent` 是按钮 + popover,不在输入区平铺 Hermes / Reasonix / Chat-only chip。
- `上下文` 是按钮 + popover,不在输入区平铺 6 个 context chip。
- 授权区域只在上下文 popover 中展示,不再在输入区显示 `write · /mnt/Data1T/mnote` 黑色提示。
- 输入区不显示 profile、model、gateway、runtime、tool trace 等技术噪音。
### 4.2 Agent popover
Agent popover 显示:
```text
选择 Agent
(x) Reasonix
适合工作区任务和文件编辑
( ) Hermes
适合 MNote 内置工具和页面操作
( ) Chat-only
只聊天,不申请文件读写能力
```
选择 agent 后:
- payload 显式携带 `agentId`
- `chat_only` 强制不挂载 MNote context tools。
- Hermes / Reasonix 可挂载 skill/tool capability,但是否读取由 agent 决定。
### 4.3 Context popover
Context popover 显示:
```text
发送给 AI 的上下文权限
[x] 当前页
允许 agent 按需读取当前 Markdown 页面
[ ] 选区
当前没有选区
[x] 打开资源
允许 agent 按需读取当前打开资源
[ ] 文件
允许 agent 按需读取当前文件
[ ] 文件夹
允许 agent 在授权根内按需读取文件夹
[ ] 最近修改
允许 agent 读取上次 run 的 changed files 摘要
授权区域
write · /mnt/Data1T/mnote
[恢复默认] [完成]
```
语义:
- 勾选项表达“允许 agent 读取的上下文范围”,不是“强制把内容发给模型”。
- `current_page` 勾选后,只代表工具可读取当前页;页面正文不默认塞进 prompt。
- `selection` 勾选且有选区时,agent 可通过 tool 读取选区文本;选区文本不默认塞进 prompt,除非后续用户明确选择“直接附加选区文本”模式。
- `folder` 必须由服务端 SQLite grants 重算 allowed roots。
## 5. MNote Skill Library 合同
### 5.1 Skill registry
新增 MNote 专用 skill registry,第一阶段可以用静态文件或 Rust 静态 registry,后续可落 SQLite / filesystem。
推荐初始技能:
| skill id | 简述 | 默认 agent |
| --- | --- | --- |
| `mnote-current-page` | 如何读取当前页、判断 dirty/conflict、刷新页面 | Hermes / Reasonix |
| `mnote-selection` | 如何读取和使用选区 | Hermes / Reasonix |
| `mnote-local-file` | 如何解析 MNote target / allowed roots,并让 agent 使用自身文件能力读写本地 Markdown | Reasonix |
| `mnote-attachments` | 如何处理 Markdown 标准链接、图片、PDF/Office 附件 | Hermes / Reasonix |
| `mnote-workspace-search` | 如何在授权 workspace 内搜索文件和引用 | Reasonix |
| `mnote-agent-receipt` | agent 写入后如何报告 changed files 与刷新需求 | Hermes / Reasonix |
| `mnote-chat-only` | 只聊天时不得调用 MNote 文件/页面工具 | Chat-only |
Skill 元数据:
```json
{
"id": "mnote-local-file",
"title": "MNote local file editing",
"description": "Resolve MNote targets and allowed roots before using the agent runtime's native local file tools.",
"agentIds": ["reasonix", "hermes"],
"readOnly": false,
"requiresContextRefs": ["current_page", "file", "folder"],
"toolNames": ["mnote.context.snapshot", "mnote.context.resolve_target"],
"path": "skills/mnote-local-file/SKILL.md"
}
```
### 5.2 Prompt 中只出现摘要
Page AI 上游 prompt 中最多允许出现类似摘要:
```xml
<available-skills>
Use mnote_skill.read to load the full content of any skill listed below.
- mnote-current-page — Read the current MNote Markdown page when the task needs page content.
- mnote-local-file — Resolve MNote targets and then use native file tools inside allowed roots.
- mnote-attachments — Resolve and open Markdown attachment links.
</available-skills>
```
禁止:
- 把每个 skill 的完整正文默认塞入 prompt。
- 把当前页全文默认塞入 prompt。
- 把 runTargetSnapshot 的正文、pageXml、contextBlocks 默认塞入 prompt。
- 把工具长说明拼进用户消息。
### 5.3 Skill 读取工具
新增或适配工具:
```json
{
"name": "mnote_skill.read",
"description": "Read a MNote skill by id when the task requires this MNote capability.",
"input_schema": {
"type": "object",
"properties": {
"skillId": { "type": "string" }
},
"required": ["skillId"]
}
}
```
返回:
```json
{
"skillId": "mnote-local-file",
"content": "...SKILL.md...",
"tools": ["mnote_context.resolve_target", "mnote_file.read", "mnote_file.patch"],
"constraints": {
"allowedRootsRequired": true,
"mustReadBackAfterWrite": true
}
}
```
## 6. MNote Context Tool 合同
### 6.1 工具列表
第一阶段建议提供最小工具集:
| tool | 职责 |
| --- | --- |
| `mnote_context.snapshot` | 返回本次 run 可用上下文摘要,不含正文全文 |
| `mnote_context.read_current_page` | 在 `current_page` 被授权时读取当前页 Markdown |
| `mnote_context.read_selection` | 在 `selection` 被授权且存在时读取选区 |
| `mnote_context.resolve_target` | 返回当前 rootUri、relativePath、documentId、file version |
| `mnote_file.read` | 在 allowed roots 内读取文件 |
| `mnote_file.patch` | 在 allowed roots 内 patch 文件,写入后回读 |
| `mnote_receipt.changed_files` | agent 完成后提交 changed files 或读取 audit 结果 |
### 6.2 权限原则
- 前端 `allowedRoots` 只作为 UI 展示与用户选择,不作为服务端真相。
- 服务端必须按当前 SQLite user session 重算 grants。
- 每次 tool 调用都按 `user_id + workspace_id + contextRefs + allowedRoots` 判定。
- `chat_only` 不注册文件读写工具。
- 未勾选 `current_page` 时,`mnote_context.read_current_page` 返回 blocked。
- 未勾选 `folder` 时,`mnote_file.read` 只能读 primary target 或当前文件。
### 6.3 上下文快照
`mnote_context.snapshot` 返回示例:
```json
{
"schema": "mnote.context_snapshot.v1",
"runId": "page-ai-run-abc",
"agentId": "reasonix",
"workspace": {
"sourceKind": "local_folder",
"rootUri": "file:///mnt/Data1T/mnote"
},
"contextRefs": ["current_page", "active_editor"],
"primaryTarget": {
"documentId": "local-md:README.md",
"relativePath": "README.md",
"fileVersion": "mtime:size:hash"
},
"availableReads": {
"currentPage": true,
"selection": false,
"folder": false,
"changedFiles": false
}
}
```
注意:snapshot 是工具返回,不是默认 prompt 注入。
## 7. AgentRunEnvelope 与 Receipt 的新位置
`AgentRunEnvelope` 不废弃,但职责调整:
- 宿主侧构建,用于 audit、权限判定、工具输入和 receipt 关联。
- 可以被 `mnote_context.snapshot` 精简返回。
- 不默认进入 ACP `prompt_blocks`
- 不作为普通聊天请求的 system instructions。
`AgentRunReceipt` 仍保留:
- run 完成后记录 changed files、touchesCurrentFile、shouldRefreshEditor、conflict。
- clean buffer 下 agent 写入当前 `.md` 后触发事件驱动刷新。
- dirty buffer 下进入 `external-change-conflict`
- 不通过轮询等待结果;使用 ACP events、watcher、WS/SSE 或现有浏览器事件。
## 8. 后端数据流
```text
Page AI composer
-> 用户选择 Agent / contextRefs
-> sendPageAiMessage()
-> payload: agentId + contextRefs + editorTarget + runTargetSnapshot metadata
-> 服务端认证 + SQLite grants 重算
-> 构建 host-side AgentRunEnvelope
-> 判断是否挂载 MNote skill/tool capability
- chat_only: 不挂载
- simple chat: 不挂载
- Hermes/Reasonix + 文档任务: 挂载 skill summary + context tools
-> ACP run prompt_blocks 只包含用户原文
-> agent 按需调用 mnote_skill.read / mnote_context.* / mnote_file.*
-> run completed
-> AgentRunReceipt / agentAudit.changedFiles
-> 事件驱动刷新当前页、文件树、页面树或显示 conflict
```
## 9. “是否挂载能力”的判定
第一阶段采用保守规则:
暴露 MNote skill/tool capability
- agentId 是 `hermes``reasonix`
- 用户在 context popover 中勾选了 `current_page / selection / active_editor / file / folder / changed_files` 这类 MNote 上下文能力。
- 用户 prompt 原文只用于 agent 自己推理,不由 MNote 侧按关键词分类成“普通聊天 / 文档任务”。
不挂载:
- agentId 是 `chat_only`
- 用户没有勾选任何 MNote contextRef。
- 当前没有有效 workspace / rootUri / session。
能力暴露不等于工具调用。MNote 只提供边界、授权和 skill/tool 描述;是否读取当前页、文件或附件由 agent 根据任务自行选择。
## 10. 与当前半成品实现的差异
当前半成品中已有一些方向需要修正:
- `acp_stream_events` 已改为 prompt_blocks 只放用户原文,这是正确方向。
- `should_skip_mnote_tool_context` 这类短句特判不应保留;应升级成只看 agentId / contextRefs / 授权边界的 capability attach policy。
- `build_run_upstream_body(...).instructions` 仍可作为 legacy Hermes route 兼容,但不应进入 ACP prompt。
- `toolGuidance` 不应变成长文本默认 instructions;应拆入 skill 正文,按需读取。
- `AgentRunEnvelope` 应保留为 audit/tool input,不作为默认 prompt 文本。
## 11. 验收 Checklist
### Batch A:冻结现状与 PilotDeck 对照
- [x] 记录当前“收到请回复收到”触发工具调用的真实浏览器证据。证据:用户截图/反馈记录了旧 UI 下短消息出现大量工具调用;本轮用 `task502-page-ai-agent-selector-context-smoke` 固化回归断言:短消息无 tool card,payload 不默认上传正文。
- [x] 记录当前 ACP payload / prompt_blocks 是否包含 MNote instructions。证据:`acp_stream_events` 现只构造用户原文 `ContentBlock::Text`,不再调用 `build_run_upstream_body(...).instructions` 塞入 ACP prompt。
- [x] 对照 PilotDeck `PromptAssembler.formatSkills()`,确认 MNote 只注入 skill 摘要。证据:参考 `reference-code/PilotDeck/src/context/prompt/PromptAssembler.ts`MNote 新增 skill registry 摘要与 `mnote.skill.read` 懒加载。
- [x] 对照 PilotDeck `InputProcessor`,确认 slash/command 不强行注入所有 command body。证据:参考 `reference-code/PilotDeck/src/context/input/InputProcessor.ts`,本轮未把 MNote skill 正文塞入普通用户 prompt。
### Batch BSkill registry 设计与最小实现
- [x] 新增 MNote skill metadata registry。证据:`rust/crates/mnote-web/src/hermes_tools/skill.rs`
- [x] 新增 `mnote_skill.read` 或等价工具。证据:`mnote.skill.read` manifest + route dispatch。
- [x] 添加 `mnote-current-page``mnote-local-file``mnote-chat-only` 三个最小 skill 正文。证据:`skills/mnote-*/SKILL.md`
- [x] 单测覆盖 skill id lookup、未知 skill、agentId 过滤。证据:`cargo test --manifest-path rust/Cargo.toml -p mnote-web skill_lookup -- --nocapture`
### Batch CContext tools 合同
- [x] 新增或收口 `mnote_context.snapshot`。证据:`mnote.context.snapshot` manifest + `context_tools::context_snapshot`
- [x] 新增或收口 `mnote_context.read_current_page`。证据:`mnote.context.read_current_page` manifest + current_page contextRef guard。
- [x] 新增或收口 `mnote_context.resolve_target`。证据:`mnote.context.resolve_target` manifest + route dispatch。
- [x] 工具返回结构化 JSON,不把正文混入 prompt。证据:context snapshot / resolve target 只返回 JSON;读取正文必须显式调用 read_current_page。
- [x] Rust 定点测试覆盖 contextRefs 权限。证据:`cargo test --manifest-path rust/Cargo.toml -p mnote-web context_ref_enabled -- --nocapture`
### Batch DCapability attach policy
- [x] 实现 `PageAiCapabilityPolicy` 或等价函数。证据:`page_ai_capability_policy`
- [x] `chat_only` 永不挂载 MNote 文件/页面工具。证据:policy 单测 + Reasonix wrapper chatLoop 使用空 ToolRegistry。
- [x] MNote 不按“收到/你好/总结当前页”等 prompt 文本强行分类能力;普通消息是否调用工具由 agent 自己判断。证据:`page_ai_capability_policy_exposes_selected_context_without_prompt_classifying``capability_policy_does_not_text_classify_ack_prompt`
- [x] 只在 Hermes / Reasonix 且用户勾选 MNote contextRefs 时暴露 skill/tool capability;没有 contextRefs 或 Chat-only 时不挂载。证据:`page_ai_capability_policy_does_not_attach_without_context_refs``page_ai_capability_policy_never_attaches_for_chat_only`
- [x] 单测覆盖 selected contextRefs、无 contextRefs、Chat-only、ack prompt 不文本分类四类输入。证据:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_capability_policy -- --nocapture`
### Batch EACP prompt 收口
- [x] ACP `prompt_blocks` 只包含用户原文和用户显式附件,不包含 MNote tool guidance。证据:`acp_stream_events` 只传 `input.to_string()`
- [x] skill 摘要如果需要进入 system/context,只作为短 `<available-skills>`。证据:Reasonix wrapper mnoteLoop system prompt 仅保留短摘要,正文走 `mnote_skill_read`
- [x] `AgentRunEnvelope` 不进入 prompt_blocks。证据:ACP path 不再使用 upstream instructions。
- [x] `build_run_upstream_body` 的 legacy instructions 不再被 ACP path 直接使用。证据:`acp_stream_events` 不调用 `build_run_upstream_body`
### Batch FUI 收口
- [x] Agent selector 改为 `+` 旁边的 Agent 按钮 + popover。证据:`task502-page-ai-agent-selector-context-smoke` 通过。
- [x] Context refs 改为 `+` 旁边的上下文按钮 + popover。证据:`task502-page-ai-agent-selector-context-smoke` 通过。
- [x] 输入区不显示 `write · /path` 黑色授权提示。证据:截图 `tmp/task502-page-ai-agent-selector-context-smoke/01-agent-selector-context.png`
- [x] 默认聊天面不显示 profile/model/gateway/runtime/tool trace。证据:`task502-page-ai-agent-selector-context-smoke` 断言通过。
- [x] 浏览器截图验证 popover、选中态、输入区清爽度。证据:`tmp/task502-page-ai-agent-selector-context-smoke/01-agent-selector-context.png`
### Batch GTool card 降噪
- [x] 普通聊天不显示 MNote tool card。证据:`task502-page-ai-agent-selector-context-smoke` 对“收到请回复收到”断言无 tool card。
- [x] Page AI run payload 不默认上传 `pageText/pageXml/contextBlocks/selectedText/evidence` 正文;当前页/选区正文必须由 agent 通过 context tools 按需读取。证据:`pageAiPageContextForRefs()` 默认裁剪正文,`task502-page-ai-agent-selector-context-smoke` 断言通过。
- [x] tool 调用失败时卡片默认折叠,显示简短错误和展开入口。证据:tool message 使用 `<details class="wolai-page-ai-tool-details">`,默认不加 `open`
- [x] debug/raw trace 进入高级/调试区,不污染默认聊天。证据:trace/auditId 只在 tool `<details>` 内显示;默认聊天 smoke 截图无 runtime/raw trace。
- [x] “收到请回复收到”真实浏览器测试只显示用户消息与 assistant 回复。证据:`task502-page-ai-agent-selector-context-smoke` 通过并生成截图。
### Batch HReceipt 与事件驱动刷新
- [x] 保留 `AgentRunReceipt` / `agentAudit.changedFiles`。证据:`local_agent_audit_event` 写入 `agentRunReceipt``cargo test --manifest-path rust/Cargo.toml -p mnote-web local_agent_audit_event_carries_agent_run_receipt -- --nocapture`
- [x] clean buffer agent 写入后事件驱动刷新当前页。证据:前端优先消费 `agentRunReceipt.refresh.touchesCurrentFile` 并派发 `mnote:page-ai-tool-write-completed``document-session-runtime.js` 负责 clean refresh / dirty conflict。
- [x] dirty buffer agent 写入后进入 conflict。证据:`document-session-runtime.js``refreshDocumentSessionsFromExternalWrite` 对 dirty / saving / recent input 标记 `external-change-conflict`,事件来源改为 receipt。
- [x] 不引入定时轮询。证据:本轮只使用 ACP SSE `run.completed`、浏览器 CustomEvent、现有 watcher/WS/SSE 事件,没有新增 `setInterval` 或轮询。
- [x] changed files 与文件树/页面树刷新走 watcher、ACP event、WS/SSE 或现有事件。证据:`agentRunReceipt.changedFiles` 转为 `tree:local-folder-watch-batch`,由 `sidebar-tree-live-apply-runtime.js` 事件驱动刷新 affected parents。
### Batch I:验证矩阵与归档
- [x] Rust targeted testsskill registry、capability policy、context tool auth。证据:`page_ai_capability_policy``skill_lookup``context_ref_enabled` 定点测试通过。
- [x] JS `node --check`Page AI runtime 与 smoke。证据:`node --check scripts/reasonix-acp-wrapper.mjs rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js rust/crates/mnote-web/browser/sidebar-tree-runtime.js scripts/task502-page-ai-agent-selector-context-smoke.js`
- [x] 浏览器 smokeAgent popover、Context popover、纯聊天无工具调用、payload 不默认上传正文、receipt 事件驱动刷新。证据:`node scripts/task502-page-ai-agent-selector-context-smoke.js`
- [x] 浏览器截图:输入区不显示授权黑 chip。证据:`tmp/task502-page-ai-agent-selector-context-smoke/01-agent-selector-context.png`
- [x] `git diff --check`
- [x] `codegraph sync .`,必要时 `codegraph index . --force`
- [x] checklist 全部完成后移动到 `design/07-ai/done/`
## 12. 归档条件
满足以下条件后,本稿可移动到 `design/07-ai/done/`
- Page AI 已采用 MNote skill library + context tools 模型。
- ACP 用户 prompt 保持干净,不再默认注入 MNote instructions。
- 纯聊天请求不会触发 MNote doc/page/file 工具。
- 文档任务仍能通过 skill/tool 按需读取当前页或文件。
- Agent / Context UI 已收敛为输入区按钮 + popover。
- 真实浏览器截图与 smoke 证明默认聊天面没有冗余技术噪音。
@@ -0,0 +1,425 @@
# 7-41 Page AI Hermes / Reasonix user profile isolation v2
> 创建时间:2026-05-29
>
> 状态:`process`
>
> OwnerPage AI agent identity / Hermes profile policy / Reasonix memory policy
>
> 上位依据:
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
> - `design/07-ai/done/7-40-page-ai-context-envelope-and-run-receipt-v1.md`
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
> - `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`
>
> 参考依据:
> - Hermes 官方 profile 文档:profile 是独立 Hermes home,包含 `config.yaml`、`.env`、`SOUL.md`、memories、sessions、skills、cron、state database、gateway state。
> - Hermes WebUI:技能 toggle 直接写 profile `config.skills.disabled`。
> - Hermes VSCode:只管理会话和一次性上下文建议,不提供真正的 per-user skill disable。
> - PilotDeck:以 WorkSpace 为边界隔离文件、记忆和技能,并强调白盒记忆可追溯。
> - Reasonix 文档/本地实现:默认使用 `~/.reasonix/memory/global` 与 `~/.reasonix/memory/<project-hash>``REASONIX_MEMORY=off|false|0` 可关闭 memory 注入。
## 1. 背景
`7-39` 已把 Page AI UI 收口为 agent selector + contextRefs + SQLite per-user preference。`7-40` 进一步把 MNote skill library 定位为 agent 可按需读取的能力摘要,而不是每轮强行注入 prompt。
但当前 Hermes skill/profile 的真实边界仍不正确:
- Hermes 的 `skills.disabled` 是 profile 级配置,不是 MNote 用户级偏好。
- Hermes profile 自带 `SOUL.md`、memory、session、skills 和 state;多个 MNote 用户共用同一个可写 profile,会把个人偏好、记忆和技能配置混在一起。
- 当前 Page AI UI 已经能展示 Hermes skill toggle,但普通用户如果直接写共享 Hermes profile,就会影响其它用户。
- Reasonix 虽然不像 Hermes 那样有 SOUL 人格,但它也有 global/project memory。默认共享 `~/.reasonix` 时,不能假设完全无状态。
因此本稿把 Page AI agent identity 从“选择 Hermes / Reasonix”提升为“选择 agent + profile scope + memory policy”。
## 2. 产品决策
### 2.1 Hermes profile 分层
Hermes profile 分为两类:
| profile kind | owner | 普通用户能否使用 | 普通用户能否改 skill | memory/session |
| --- | --- | --- | --- | --- |
| `personal` | 单个 MNote 用户 | 能 | 能,仅限自己的 profile | 用户独立 |
| `shared` | 系统 / 管理员 | 能,若管理员公开 | 不能 | 默认不写入用户私有长期记忆 |
初始共享 Hermes profile 只包含 `lite`
要求:
- 每个 MNote 用户可以访问自己的 personal Hermes profile。
- 每个 MNote 用户可以访问被管理员公开的 shared Hermes profile。
- shared Hermes profile 的 skill/config 只有管理员能改。
- personal Hermes profile 的 skill/config 只有该 profile owner 或管理员能改。
- Page AI 发送请求时必须携带解析后的 `agentProfileRef`,不能只携带裸 `profile=lite`
### 2.2 MNote 内置技能开关
MNote 内置 skill / tool 属于 MNote 自己的能力面,普通用户应该可以按用户启停。该开关不写 Hermes profile,而是写 SQLite control-plane。
含义:
- `mnote_builtin_skill_enabled` 是 per-user 偏好和服务端执行策略,必须按当前 SQLite 用户隔离。
- `hide_builtin_skills=true` 只是 UI 展示偏好,用于折叠或隐藏 MNote 内置能力说明;它不等于禁用。
- MNote tool 是否可调用由服务端根据 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 决定。
- 普通用户可启停自己的 MNote 内置 skills;管理员可设置默认值或全局禁用策略。
- MNote 内置 skill 开关不修改 Hermes profile `config.yaml`,也不影响 shared Hermes profile。
### 2.3 Hermes profile skills
可以配置的只有 Hermes profile 自己的 skills。
规则:
- personal profile:用户可启停该 profile 下的 Hermes skills,写入该 personal profile 的 `config.yaml`
- shared profile:普通用户只读;管理员可启停 shared profile skills。
- skill toggle API 必须做服务端权限检查,不能只靠 UI 禁用按钮。
- MNote 不再把 `ai.agent.hermes.profile.<name>.skills.enabled` 当成普通用户对共享 profile 的安全开关;MNote 内置 skill 的 per-user 开关应使用独立 SQLite key。
### 2.4 Reasonix memory policy
Reasonix 默认关闭 memory 注入。
规则:
- 默认启动 Reasonix ACP 时设置 `REASONIX_MEMORY=off`
- 每个 MNote 用户可在设置中开启 Reasonix memory。
- 该设置保存到 SQLite `user_ui_preferences`,按用户隔离。
- 开启后,UI 需明确标注 Reasonix 将使用 global/project memory。
- 第一阶段不强制给 Reasonix 每用户独立 HOME;若后续需要更强隔离,再增加 `reasonix_home_mode=per_user`
## 3. 数据合同
### 3.1 Agent profile ref
Page AI payload 中新增或固定以下结构:
```json
{
"agentId": "hermes",
"agentProfileRef": {
"kind": "personal",
"profileId": "usr_123_default",
"ownerUserId": "usr_123",
"baseProfile": "default",
"displayName": "我的 Hermes"
}
}
```
shared profile
```json
{
"agentId": "hermes",
"agentProfileRef": {
"kind": "shared",
"profileId": "shared_lite",
"ownerUserId": null,
"baseProfile": "lite",
"displayName": "Lite"
}
}
```
服务端要求:
- 不接受浏览器直接提交任意 Hermes filesystem path。
- `profileId` 必须由 SQLite control-plane 解析到当前用户可访问的 profile。
- 对 personal profile,当前用户必须是 owner 或 admin。
- 对 shared profile,必须是公开 profile 或 admin。
- 所有 run / skill list / skill toggle / settings read 都通过 `agentProfileRef` 解析,不走裸 profile name。
### 3.2 SQLite control-plane
建议新增控制面表,避免只用偏好 key 表达权限关系:
```text
ai_agent_profiles
- id
- agent_id # hermes
- profile_kind # personal | shared
- owner_user_id # personal 必填,shared 为空
- base_profile_name # Hermes 原始 profile 名或模板名
- isolated_profile_name # MNote 管理后的真实 Hermes profile 名
- display_name
- status # active | disabled
- created_at
- updated_at
ai_agent_profile_grants
- profile_id
- user_id
- role # owner | user | admin
- can_run
- can_manage_skills
- can_manage_config
- created_at
- updated_at
```
第一阶段也可以在现有 `user_ui_preferences` 中保存默认选择:
- `ai.agent.hermes.default_profile_id`
- `ai.agent.hermes.hide_builtin_skills`
- `ai.agent.mnote_builtin.skills.enabled`
- `ai.agent.reasonix.memory_enabled`
但 profile 权限、owner、shared/personal 类型不应只存在于 UI preference。
MNote 内置 skill 的用户级开关可以第一阶段存在 `user_ui_preferences`,但服务端 tool policy 读取时必须视为执行策略,而不是纯 UI 状态。若后续需要审计、管理员默认值或组织策略,应升级为独立表:
```text
ai_user_skill_preferences
- user_id
- skill_id
- enabled
- updated_at
```
### 3.3 Hermes profile provisioning
创建 personal Hermes profile 时:
- 可从共享模板复制 `config.yaml``.env``SOUL.md` 和 skills。
- 不复制 memories、sessions、state database、gateway state。
- 生成的真实 profile name 必须包含 MNote 用户隔离标识,例如 `mnote-u-<userId>-default`
- provisioning 过程由服务端执行,并记录到 SQLite control-plane。
shared `lite`
- 初始由管理员登记为 `shared_lite`
- 普通用户只可 run / list readonly。
- 管理员可改 skill/config。
## 4. API 合同
### 4.1 Profile list
`GET /api/ai/agent-profiles?agentId=hermes`
返回当前用户可访问 profiles:
```json
{
"profiles": [
{
"profileId": "usr_123_default",
"kind": "personal",
"displayName": "我的 Hermes",
"canRun": true,
"canManageSkills": true
},
{
"profileId": "shared_lite",
"kind": "shared",
"displayName": "Lite",
"canRun": true,
"canManageSkills": false
}
]
}
```
### 4.2 Skill list
`GET /api/hermes/client/skills?profileId=...`
要求:
- personal profile 返回可 toggle 状态。
- shared profile 对普通用户返回 readonly 状态。
- MNote 内置 skills 返回值必须标记 `builtin=true``configurable=true``configScope=user_sqlite`
- Hermes profile skills 标记 `builtin=false``configurable=canManageSkills`
### 4.3 Skill toggle
`PUT /api/hermes/client/skills/toggle`
请求:
```json
{
"profileId": "usr_123_default",
"skillName": "writer",
"enabled": false
}
```
服务端必须:
- 解析 `profileId`
-`skillKind=mnote_builtin`,写当前用户 SQLite skill preference,不写 Hermes profile。
-`skillKind=hermes_profile`,校验 `canManageSkills=true`
- 拒绝普通用户修改 shared profile 的 Hermes profile skills。
- personal profile skill toggle 只写目标 Hermes profile 的 `config.yaml`
错误码建议:
- `ai_profile_not_found`
- `ai_profile_forbidden`
- `ai_profile_readonly`
- `ai_builtin_skill_preference_failed`
- `hermes_skill_toggle_failed`
### 4.4 Reasonix run
Reasonix ACP spawn / session create 需读取当前用户设置:
```json
{
"agentId": "reasonix",
"memoryPolicy": {
"enabled": false,
"source": "user_ui_preferences"
}
}
```
默认:
- `enabled=false`
- 子进程环境包含 `REASONIX_MEMORY=off`
开启:
- 不设置 `REASONIX_MEMORY=off`,或设置为 `on`
- UI 明确展示 memory 已开启
## 5. UI 设计
### 5.1 Agent selector
Hermes agent 下增加 profile 子选择:
```text
Hermes
我的 Hermes personal · 可配置
Lite shared · 只读
```
显示规则:
- personal profile 显示“可配置”。
- shared profile 显示“共享 / 只读”。
- 若普通用户选择 shared profile,技能开关显示为只读。
- 管理员选择 shared profile,技能开关可用,并显示“管理员正在修改共享 profile”。
### 5.2 Skills panel
三组仍保留:
- MNote 内置技能:可折叠,可隐藏/显示,也可由普通用户按自己账号启停。
- Hermes 技能:随当前 Hermes profile 变化;personal 可配置,shared 普通用户只读。
- Reasonix 技能:展示可用能力;memory 是单独设置,不混入 skill toggle。
Hermes profile 切换时:
- 必须重新加载 profile skills。
- 必须清空旧 profile skill cache。
- `hide_builtin_skills` 不随 Hermes profile 改变;它是用户 UI 偏好。
- MNote 内置 skill enable/disable 不随 Hermes profile 改变;它是当前 MNote 用户的 SQLite policy。
### 5.3 Settings
设置页拆分:
- Common:授权区域、contextRefs 默认值、内置技能显示/隐藏、内置技能启停。
- Hermes:默认 Hermes profile、personal profile 管理、shared profile 只读/管理员管理。
- Reasonix:默认关闭 memory;用户可开启。
- Chat-only:只聊天配置。
## 6. 非目标
- 不让普通用户直接修改 shared Hermes profile。
- 不把 shared Hermes profile 用作沉淀个人偏好的长期人格。
- 不在本阶段实现 Reasonix per-user HOME;只实现默认 memory off 与可选开启。
- 不新增第二套目录授权真相;文件访问仍由 SQLite directory grants / allowedRoots 控制。
- 不实现 PilotDeck 的完整 router、always-on 或 memory engine。
## 7. Checklist
### Batch A - 现状冻结与风险取证
- [ ] 复核当前 Page AI Hermes skill toggle 的真实写入路径,确认是否直接写 Hermes profile `config.yaml`
- [ ] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabled、profile skill enabled、default profile 的存储键。
- [ ] 复核 Reasonix ACP spawn 环境,确认当前是否默认注入 memory。
- [ ] 形成 RED 证据:普通用户修改 shared profile skill 会影响其它用户,或当前缺少服务端权限边界。
- [ ] 验证:Rust/JS 只读审计记录在本文档或后续 checklist evidence 中。
### Batch B - SQLite profile policy 合同
- [ ] 新增或扩展 SQLite control-plane profile policy`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
- [ ] 初始化 shared Hermes profile:仅 `lite`,普通用户 `canRun=true``canManageSkills=false`
- [ ] 为每个用户 provision personal Hermes profile。
- [ ] 补 Rust 定点测试:personal owner、shared readonly、admin manage、跨用户不可管理。
- [ ] 验证:不同用户查询 profile list 只返回自己 personal + shared lite。
### Batch C - Hermes profile resolver
- [ ] 新增服务端 `agentProfileRef` resolver,禁止前端提交任意 Hermes path。
- [ ] `/api/hermes/client/runs``profileId` 解析真实 Hermes profile。
- [ ] `/api/hermes/client/skills``profileId` 解析真实 Hermes profile。
- [ ] 保留旧 `profile=` 参数只作为兼容入口,并映射到当前用户可访问 profile。
- [ ] 验证:旧路径兼容不允许越权访问 shared/admin profile。
### Batch D - Skill toggle 权限收口
- [ ] 修改 skill toggle API:只接受 `profileId + skillName + enabled`
- [ ] 区分 `mnote_builtin``hermes_profile` skill kind。
- [ ] MNote 内置 skill toggle 写当前用户 SQLite preference。
- [ ] 拒绝普通用户修改 shared profile 的 Hermes profile skills。
- [ ] personal profile skill toggle 只写该用户 isolated profile `config.yaml`
- [ ] shared profile skill toggle 仅 admin 可写。
- [ ] 验证:Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success。
### Batch E - MNote 内置 skill per-user policy
- [ ] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
- [ ] UI 中 `hide_builtin_skills` 只影响展示,不影响 enable/disable。
- [ ] Skills panel 标记 `builtin=true``configurable=true``configScope=user_sqlite`
- [ ] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断。
- [ ] 验证:用户 A 禁用某内置 skill 不影响用户 B;禁用后对应 MNote tool 被服务端拒绝;隐藏展示不影响 enable 状态。
### Batch F - Reasonix memory policy
- [ ] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`,默认 `false`
- [ ] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`
- [ ] 开启 memory 后不注入 `REASONIX_MEMORY=off`,并在 UI 显示 memory enabled。
- [ ] 补 JS/Rust 测试或 smoke,覆盖默认 off、用户开启、不同用户隔离 preference。
- [ ] 验证:普通消息由 Reasonix 自己决定是否使用工具;MNote 不再强行注入 memory/context 正文。
### Batch G - UI 收口
- [ ] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
- [ ] Skills panel 三组均可折叠。
- [ ] Hermes profile 切换必须刷新 skill catalog,避免旧 profile skill 残留。
- [ ] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
- [ ] MNote 内置 skills 对普通用户展示可启停状态,并标明按当前 MNote 用户保存。
- [ ] 管理员对 shared profile 显示可管理状态,并提示影响所有用户。
- [ ] 验证:真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
### Batch H - 回归矩阵与文档收尾
- [ ] 更新 Page AI 设计说明,明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
- [ ] 更新 smokeagent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
- [ ] 运行 `node --check` 覆盖相关 browser runtime / smoke。
- [ ] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
- [ ] 运行真实浏览器验证并截图。
- [ ] 运行 `git diff --check`
- [ ] 涉及代码图后运行 `codegraph sync .`
- [ ] 完成后将本 checklist 移动到 `done/` 或标记为 `done`
## 8. 验收口径
完成后必须满足:
- 普通用户可以启停自己的 MNote 内置 skills,开关保存到 SQLite 并由服务端 tool policy 执行。
- 用户 A 的 MNote 内置 skill 开关不影响用户 B。
- 普通用户无法修改 shared `lite` 的 Hermes profile skills。
- 普通用户可以修改自己的 personal Hermes profile skills。
- MNote 内置 skill 的显示/隐藏与启停是两个不同状态:隐藏只影响 UI,启停影响服务端可调用性。
- Hermes profile 切换后 skill catalog 正确刷新。
- Reasonix 默认 memory off,开启 memory 是 per-user preference。
- Page AI run payload 不再用裸 Hermes profile name 表达身份,而是经服务端解析的 `agentProfileRef` / `profileId`
- 所有文件访问仍受 SQLite directory grants / allowedRoots 约束。