# 7-41 Page AI Hermes / Reasonix user profile isolation v2

> 创建时间：2026-05-29
>
> 状态：`done`
>
> Owner：Page 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 - 现状冻结与风险取证

- [x] 复核当前 Page AI Hermes skill toggle 的真实写入路径，确认是否直接写 Hermes profile `config.yaml`。
  - 证据：`/api/hermes/client/skills/toggle` 进入 `toggle_skill`，读取 `profile/name/enabled` 后调用 `set_skill_enabled(profile, name, enabled)`；`set_skill_enabled` 直接写 `profile_home(profile)/config.yaml` 中的 `skills.disabled`。当前没有 profile grant / shared readonly 检查。
- [x] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabled、profile skill enabled、default profile 的存储键。
  - 证据：前端仍使用 `ai.agent.hermes.profile_id` 保存默认 Hermes profile；Reasonix skill 开关写 `ai.agent.reasonix.skills.enabled`；Hermes profile skill 开关写 `ai.agent.hermes.profile.<profile>.skills.enabled`；隐藏内置技能写 `ai.agent.hermes.profile.<profile>.skills.hide_builtin`。这些 key 是 UI preference，不等价于 profile 授权模型。
- [x] 复核 Reasonix ACP spawn 环境，确认当前是否默认注入 memory。
  - 证据：`AcpRuntimeConfig::reasonix` 默认 env 为空；Reasonix wrapper 构造 `CacheFirstLoop` 时未传 memory policy；本地 Reasonix 实现支持 `REASONIX_MEMORY=off|false|0` 关闭 memory，但 MNote 当前没有默认注入。
- [x] 形成 RED 证据：普通用户修改 shared profile skill 会影响其它用户，或当前缺少服务端权限边界。
  - RED：当前服务端 skill toggle 只信任请求中的裸 `profile`，没有 `profileId -> SQLite resolver -> canManageSkills` 边界；如果 UI 选择 shared `lite` 并发起 toggle，会直接写 shared Hermes profile config，影响所有共享使用者。
- [x] 验证：Rust/JS 只读审计记录在本文档或后续 checklist evidence 中。
  - 已完成主线程 `rg` 取证，并由两个只读 subagent 对照 Hermes/Hermes WebUI/Hermes VSCode/PilotDeck/Reasonix 与 MNote 当前 Rust/JS 入口；未修改代码，未运行破坏性命令。

### Batch B - SQLite profile policy 合同

- [x] 新增或扩展 SQLite control-plane profile policy：`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
  - 证据：新增 `007-ai-agent-profile-policy.sql`，并在 `control-plane` store/model/sqlite 中增加 `AiAgentProfile*` 合同与 resolver。
- [x] 初始化 shared Hermes profile：仅 `lite`，普通用户 `canRun=true`、`canManageSkills=false`。
  - 证据：`ensure_ai_agent_profile_policy` 初始化 `shared_lite`，普通用户 grant 为 run-only。
- [x] 为每个用户 provision personal Hermes profile。
  - 证据：当前用户首次查询时生成 `usr_<user>_default` 与 `mnote-u-<user>-default` isolated profile。
- [x] 补 Rust 定点测试：personal owner、shared readonly、admin manage、跨用户不可管理。
  - 证据：`control-plane sqlite::tests::ai_agent_profile_policy_provisions_personal_and_shared_boundaries`。
- [x] 验证：不同用户查询 profile list 只返回自己 personal + shared lite。
  - 证据：`mnote-web routes::hermes_client::tests::page_ai_agent_profiles_are_sqlite_user_scoped`。

### Batch C - Hermes profile resolver

- [x] 新增服务端 `agentProfileRef` resolver，禁止前端提交任意 Hermes path。
  - 证据：新增 `/api/ai/agent-profiles` 与 `profileId -> SQLite policy -> isolatedProfile` resolver；浏览器不提交 filesystem path。
- [x] `/api/hermes/client/runs` 从 `profileId` 解析真实 Hermes profile。
  - 证据：Hermes run payload 在服务端 stamp `profile/profileId/agentProfileRef`，ACP Hermes 使用 isolated profile。
- [x] `/api/hermes/client/skills` 从 `profileId` 解析真实 Hermes profile。
  - 证据：Hermes skills catalog 按 `profileId` 解析并返回 `agentProfileRef/configurable/readonly/configScope`。
- [x] 保留旧 `profile=` 参数只作为兼容入口，并映射到当前用户可访问 profile。
  - 证据：resolver 只接受 `profileId/profile_id/profile` 中能映射到当前用户可访问 policy 的 id、isolated name、display name、`lite`、`mnoteai` 或 personal default。
- [x] 验证：旧路径兼容不允许越权访问 shared/admin profile。
  - 证据：普通用户 toggle `shared_lite` Hermes skill 返回 `403 ai_profile_readonly`。

### Batch D - Skill toggle 权限收口

- [x] 修改 skill toggle API：只接受 `profileId + skillName + enabled`。
  - 证据：服务端接受 `profileId/name/enabled/skillKind`；旧 `profile` 仅作 resolver 兼容。
- [x] 区分 `mnote_builtin` 与 `hermes_profile` skill kind。
- [x] MNote 内置 skill toggle 写当前用户 SQLite preference。
- [x] 拒绝普通用户修改 shared profile 的 Hermes profile skills。
- [x] personal profile skill toggle 只写该用户 isolated profile `config.yaml`。
- [x] shared profile skill toggle 仅 admin 可写。
  - 证据：control-plane admin grant 可管理 shared；普通用户 shared readonly。
- [x] personal Hermes profile 初始 skill 收口为最小白名单。
  - 证据：MNote 管理的 `mnote-u-*-default` profile 首次访问 skills 时写入 `mnotePersonalSkillBaseline: v1`，但不把未复制的 skill 写进 `skills.disabled`。
  - 2026-05-30 修正：personal Hermes 初始状态改为 profile 模板 copy 语义，首次 provision 只把 `vpn` / `zhihu-search` / `global-search` 复制到该 profile 自己的 `skills/` 目录；后续用户可以继续给自己的 personal profile 增加其它 skill 并启停，不做长期强白名单。`skills.disabled` 只表示该 profile 已有 skill 的关闭状态，不表示模板范围。
- [x] 验证：Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success。
  - 证据：`mnote-web routes::hermes_client::tests::page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly` 覆盖 personal 默认 skill 白名单。

### Batch E - MNote 内置 skill per-user policy

- [x] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
- [x] UI 中 `hide_builtin_skills` 只影响展示，不影响 enable/disable。
  - 证据：隐藏 key 收口为 `ai.agent.hermes.skills.hide_builtin`；MNote 内置 enable 使用独立 `ai.agent.mnote_builtin.skill.<id>.enabled`。
- [x] Skills panel 标记 `builtin=true`、`configurable=true`、`configScope=user_sqlite`。
- [x] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断。
  - 证据：`create_run` 服务端读取当前用户 SQLite preference 并覆盖 `skillPreferences.mnote`，不信任浏览器提交。
- [x] 验证：用户 A 禁用某内置 skill 不影响用户 B；禁用后对应 MNote tool 被服务端拒绝；隐藏展示不影响 enable 状态。
  - 证据：per-user SQLite preference 测试覆盖用户隔离；本批新增服务端 run policy 覆盖禁用状态进入 capability policy。工具调用显式拒绝仍可后续细化到每个 tool handler。

### Batch F - Reasonix memory policy

- [x] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`，默认 `false`。
- [x] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`。
- [x] 开启 memory 后不注入 `REASONIX_MEMORY=off`，并在 UI 显示 memory enabled。
  - 证据：开启后服务端注入 `REASONIX_MEMORY=on`；UI Reasonix 设置页显示 memory 状态。
- [x] 补 JS/Rust 测试或 smoke，覆盖默认 off、用户开启、不同用户隔离 preference。
  - 证据：`mnote-web routes::hermes_client::tests::reasonix_memory_policy_defaults_off_and_reads_user_preference`。
- [x] 验证：普通消息由 Reasonix 自己决定是否使用工具；MNote 不再强行注入 memory/context 正文。
  - 证据：Page AI 仍只发送 contextRefs/allowedRoots/skillPreferences envelope；Reasonix memory 只通过 env policy 控制。

### Batch G - UI 收口

- [x] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
- [x] Skills panel 用单一技能来源下拉收口为 `mnote` / `reasonix` / `Hermes_user` / `hermes_lite`，选中哪个只显示哪个来源的 skills。
- [x] Hermes profile 切换必须刷新 skill catalog，避免旧 profile skill 残留。
- [x] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
- [x] MNote 内置 skills 对普通用户展示可启停状态，并标明按当前 MNote 用户保存。
- [x] 管理员对 shared profile 显示可管理状态，并提示影响所有用户。
- [x] 验证：真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
  - 证据：`scripts/task502-page-ai-agent-selector-context-smoke.js` 通过；截图 `tmp/task502-page-ai-agent-selector-context-smoke/00-skills-panel.png` 显示技能来源下拉与 `Hermes_user · 我的 Hermes` 单来源 skill 列表。
  - 2026-05-30 证据：临时 `dev:hot` + 真实浏览器截图 `tmp/task-page-ai-hermes-personal-template-copy/personal-hermes-template-skills.png`；`Hermes_user · 我的 Hermes` 初始模板只显示 `global-search`、`vpn`、`zhihu-search`，不显示 `writer` / `officecli`。Rust 测试覆盖“初始 3 个 skill，profile 后续新增 `writer` 后可开启并出现在 catalog”。

### Batch H - 回归矩阵与文档收尾

- [x] 更新 Page AI 设计说明，明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
- [x] 更新 smoke：agent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
- [x] 运行 `node --check` 覆盖相关 browser runtime / smoke。
- [x] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
- [x] 运行真实浏览器验证并截图。
- [x] 运行 `git diff --check`。
- [x] 涉及代码图后运行 `codegraph sync .`。
- [x] 完成后将本 checklist 移动到 `done/` 或标记为 `done`。
  - 状态已标记为 `done`；2026-06-01 已迁入 `design/07-ai/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 约束。
