Files
mnote/design/12-vault/process/12-1-password-vault-dedicated-crud-workbench-v1.md
T
Agent Board b798f628ee chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
2026-07-21 05:13:05 +08:00

1026 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 12-1 [process] 密码箱(Password Vault)专用 CRUD 工作台整体设计 + Checklist v1
> 创建时间:2026-07-19
> 修订时间:2026-07-19+ 密文簿 cipher-book、folderPath 树分组、同站多账号;review 修订:path deny 合入门、软删合同、secret PATCH、刷新模型、PR 重排;re-reviewresource/read 等表面、P0-S/E 门闩、hint 仅 reject;补全 live `/api/local-folder/*` 路由盘点表)
> 状态:`PROCESS`
> Owner`12-vault`(跨 shell / AI / 安全的 **系统空间产品****不是** `04-tree-domain` 树节点能力,也非硬塞 `07-ai`)
> 建议 repo 落点:`design/12-vault/process/12-1-password-vault-dedicated-crud-workbench-v1.md`
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/AGENTS.md`
> - `design/04-tree-domain/done/4-37-trash-as-modal-workbench-v1.md`workbench 数据/API 组织参考;IA 不同:密码箱要独立 route)
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md``AiAccessScope` / allowed roots / 禁止裸读敏感路径)
> - `design/05-editor-mainline/done/5-34-local-markdown-attachment-ref-contract-v1.md`(附件引用语义 **启发**vault 附件有独立 owner,见 §2.5
> - `design/04-tree-domain/done/4-48-local-folder-markdown-resource-lifecycle-contract-v1.md`
> - `design/10-review/process/21-mvp-post-architecture-closure-checklist-v1.md`
---
## Overview
MNote 需要一等公民的 **密码箱(Password Vault**:按当前用户当前 workspace 解析的稳定系统目录,提供 **专用 CRUD 工作台页面**(完整列表 / 搜索 / 筛选 / 新建 / 编辑 / 删除 / 复制字段 / 显示隐藏密码),而不是普通 Markdown 笔记页,也不是仅靠 FileTree 打开自由 `.md`
本设计将 vault 定为 local-first **系统空间**`{workspaceRoot}/.mnote/vault/`,条目为带固定 frontmatter 模板的结构化 Markdown`mnote.kind: credential`)。
**P0 实现 owner(对齐 trash 先例,避免「Kernel」过称)**
| 层 | P0 落点 | 说明 |
|----|---------|------|
| 存储 + HTTP API + SSR + browser | **`mnote-web`** | 与 local trash 相同:系统空间 API + projection JSON**不是** `tree.*` 图节点 |
| 协议类型(可选) | `core-protocol` 最小 struct | 仅 DTO / schema 常量;**不**注册 bridge graph 节点 |
| bridge-runtime command 总线 | **P0 不做** | P1 再评估是否挂 `vault.*` 到 bridgeP0 以 `/api/vault/*` 为唯一写入口 |
前端:`/vault` 独立 route + SSR workbench + browser runtime。
**任何** 通用文件面(`/api/local-folder/files/open|stat`、**`/api/local-folder/resource/read|write`**、`assets/upload`、upload/write、Pi `mnote.local_file.*` / `resolve_file_path`)必须 **server-side deny** vault 路径——**在首次可写入 secret 的 PR merge 之前** 落地(见 §5.2、PR Plan);优先挂 **shared resolve choke**
AI 不得用通用 file 工具扫 vault,只能走分层 `mnote.vault.*`P1)。LightRAG / local search 排除 `.mnote/vault/**`。一期不做密码学保险箱;安全分级 L0/L1/L2。
---
## Background & Motivation
### 当前状态与痛点
1. **非正式密码笔记(用户工作区观察,非仓库事实)**:部分用户工作区存在 `个人/密码/` 下大量自由 Markdown 目录条目(用户环境曾观察到约 172 条量级),正文常含明文密码/token。无固定 schema、无统一入口、无 reveal 分级,且 **仍可被 FileTree / 本地搜索 / LightRAG 索引**——即使正式 vault 上线,在导入归档前该目录仍是泄露面(见 §7.4)。
2. **左栏存在可改造入口**`layout.rs` quick-actions 中 `inventory_2` 链到 `/files`。代码核实:`/files` **当前无注册 route**(死链),适合改造为「密码箱」固定入口。
3. **垃圾箱模式可借鉴但 IA 不同**4-37 是 **modal workbench**;用户明确要求密码箱是 **专门页面**。可复用 trash 的数据层:系统目录 ensure、index、SSR workbench、rootUri 鉴权。
4. **AI / HTTP 安全缺口(已核实)**
- `open_local_file` / `stat_local_file``/api/local-folder/files/open|stat`)在 workspace grant + path-escape 下可读 root 内 **任意** 相对路径,**无** `.mnote` / vault deny。
- **`read_local_resource`**`GET /api/local-folder/resource/read`)经 `resolve_local_file_open_path` 返回 JSON **`"text"` 全文**,同等无 vault deny——**比 open 更易被编辑器链路误用**。
- `write_local_resource` / `assets/upload``targetRelativePath`)可写任意相对路径。
- Pi `local_file_read` / `resolve_file_path``page_ai_pi/runtime.rs`)在 `allowed_roots` 内可读任意文件,**无** vault 排除。
- 因此仅做 JS intercept **不够**;必须 server deny 清单 + **shared resolve choke**(§5.2)。
5. **架构约束**local-first、禁止 UI 拼第二套真相、浏览器主链禁止新增 `setInterval`、RAG 必须排除 vault。
### 代码锚点(已核实)
| 锚点 | 路径 / 符号 | 含义 |
|------|-------------|------|
| 左栏 quick-actions | `ssr/pages/layout.rs` L145151 | `inventory_2``/files`(死链);改造目标 |
| ensure | `local_folder_source.rs` `ensure_default_workspace_directories` | 仅 `.mnote/trash` |
| 树忽略 `.mnote` | `should_ignore_entry` | FileTree 不可见 vault |
| open/stat | `open_local_file` / `stat_local_file` | **必须加 vault deny**stat **不**走 `resolve_local_open_path` |
| resource read/write | `read_local_resource` / `write_local_resource` | `GET/POST .../resource/read|write`read 回 **`text` 全文** — deny 合入门 |
| assets upload | `upload_local_markdown_asset` / `write_local_folder_file_upload` | `POST .../assets/upload``targetRelativePath` 禁 vault |
| path resolve choke | `resolve_local_open_path` / `resolve_local_file_open_path` | open + resource 共用;**PR-S 优先挂此** |
| Watcher | `local_folder_watcher_registry.rs` | 路径分量含 `.mnote` 时不按普通树/索引消费;**不能**假设外部改 vault 会推产品事件 |
| Trash SSR | `gateway.rs` `trash_entry` | 系统空间页先例 |
| AiAccessScope | `core-protocol/src/ai.rs` | allowed_roots / allowed_file_paths |
| Pi tools | `page_ai_pi/runtime.rs` | `PiLabToolDefinition` + `resolve_file_path` / `local_file_read|patch` |
| RAG / search | `knowledge_rag.rs` / `local_search_index.rs` | 已跳过 `.mnote`;需 vault 单测锁死 |
| DirectoryGrant | `control-plane` `capabilities_json` | 现有 ad-hoc `"ai"`/`"share"`vault.* **P1** 再落 grant 写入路径 |
| Attachment 启发 | design `5-34` | **页面 md** 附件合同;vault 附件为 **平行模型** |
---
## Goals & Non-Goals
### Goals
1. 专用页面 `/vault`:完整 CRUD(列表 + 详情/表单),非 leptos-tiptap。
2. 左栏固定入口「密码箱」:改造死链 `/files`,按当前用户 + workspace `rootUri` 解析。
3. 固定 credential 模板:核心 `title`/`url`/`username`/`password`;可选 email/apikey/token/notes/tagscookies 等走附件(P1+)。
4. 存储:`{workspaceRoot}/.mnote/vault/`frontmatter + `mnote.kind: credential`
5. **mnote-web 系统空间 API**`vault.ensure|list|get|create|update|delete|restore|purge|reveal`P0)与稳定 projection JSONP1 增加 resolve / AI tools。
6. 安全分级 L0/L1/L2**通用文件面 path deny 为 secret 写入的硬前置**。
7. RAG / local search / FileTree 默认不可见、不可索引 vault。
8. 可选导入 `个人/密码/`;不静默覆盖;双轨期风险明示。
9. AIP0 仅 deny 通用读;P1 `mnote.vault.*`P2 录入/自注册/cookies。
10. Checklist + PR Plan 可直接开工。
### Non-Goals
- 一期 **不做** at-rest 加密保险箱。
- 不做跨 workspace 云端 vault 同步。
- 不把 vault 做成 FileTree / Page Tree 节点或 `tree.*` 命令对象。
- 不把 vault CRUD 塞进 tiptap / Page Aggregate。
- 不做 OS 级自动填表扩展。
- **P0 不做** 外部进程改 vault 文件后的 live FS 刷新(见 §4.2);禁止 setInterval 伪装。
- 不把 vault 写入 LightRAG / evidence / 全局搜索 snippet。
- **不** 声称导入前 `个人/密码/` 已与 vault 同等安全。
---
## Proposed Design
### 1. 产品 IA
#### 1.1 入口
```text
左栏 quick-actions
[搜索] [关系图] [导航] [帮助] [密码箱] [打开本地文件夹] [更多]
原 inventory_2 → /files(死链)
改为 lock 图标 + 「密码箱」
href="/vault?sourceKind=local_folder&rootUri=..."
```
- **主入口**`layout.rs``/files`+`inventory_2` 改为 `/vault` + `lock`(默认;Open Q 可改 `key`),`class:active={current_nav == "vault"}`
- **href**browser 注入当前 `sourceKind` + `rootUri`(读 `data-mnote-root-uri` 等)。无 root → empty state。
- **bottom_entries**`workspace_shell` 的 templates/`inventory_2` **不**改 vault。
- **禁止**:通用 open 读 vault 文件(server deny);UI 若发现 vault 路径 → 引导 `/vault?id=` 或 403。
#### 1.2 路由
| Route | 方法 | 说明 |
|-------|------|------|
| `GET /vault` | SSR 全页 | 工作台;query`sourceKind``rootUri`、可选 `id``import=1``status=active\|deleted` |
| `/files` | 兼容 | **302 → `/vault`**(保留 query 中 root 相关参数若可解析) |
#### 1.3 页面布局
```text
┌─────────────────────────────────────────────────────────────┐
│ PageLayout topbar: 密码箱 │
│ ┌──────────┬──────────────────────────────────────────────┐ │
│ │ Sidebar │ Vault Workbench │ │
│ │ │ toolbar: 搜索 | 标签 | 新建 | 导入 | 刷新 │ │
│ │ │ tabs: [活跃条目] [回收站] │ │
│ │ │ list (40%) | detail/form (60%) │ │
│ └──────────┴──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
| 能力 | 行为 |
|------|------|
| 列表 | `vault.list` L0 projection |
| 搜索 | 客户端过滤 L0;不把 password 写入任何浏览器持久索引 |
| 新建/编辑 | create / get(mask) / update(§3.5 secret PATCH 语义) |
| 删除 | 确认 → **soft-delete**P0,§2.6 |
| 回收站 tab | list `status=deleted`restore / purgeP0 |
| 复制 / 显示密码 | copy 或 reveal;默认掩码;不写 localStorage |
| 刷新 | **仅**(1) command 成功后 local patch / re-fetch(2) 工具栏「刷新」按钮。**禁止** setInterval。**不**依赖现有 local folder watcher 推 vault 变更 |
#### 1.4 P0 UI 验收(最小)
- [ ] `?id=cred_…` deep-linkSSR/bootstrap 后选中对应行并加载详情。
- [ ] 密码区默认掩码;未点「显示」前 DOM/HTML 无 password 明文。
- [ ] dirty 表单点「取消」或切换条目:未保存则 confirm discard。
- [ ] 必填:`title` 为空时保存 disabled + 行内错误;建议引导填写 url/username/password 至少一项。
- [ ] 标签:逗号/Enter 添加 chip;删除 chip 即改 tags。
- [ ] 回收站 tab 可见已删条目;restore 回活跃列表;purge 二次确认后磁盘无文件。
- [ ] 窄屏:P0 可接受 list/detail 纵向堆叠(非阻塞);键盘快捷键非 P0。
Empty states:无 root / 0 条目(新建+导入提示,并 **警告** 源目录 `个人/密码/` 若存在仍可能被搜索/RAG 索引)/ 无权限。
---
### 2. 数据模型
#### 2.1 目录布局
```text
{workspaceRoot}/
.mnote/
vault/ # 系统 ensure
vault-index.json # L0 索引(含 status
audit.jsonl # P0 唯一审计 sinkappend-only
entries/{credentialId}.md
attachments/{credentialId}/cookies-{stableId}.txt
trash/
entries/
attachments/
```
**ensure**(扩展 `ensure_default_workspace_directories` + `vault.ensure`):
```text
.mnote/trash
.mnote/vault
.mnote/vault/entries
.mnote/vault/attachments
.mnote/vault/trash
.mnote/vault/trash/entries
.mnote/vault/trash/attachments
```
访问 `/vault` 或任意 vault 写 API 前 ensure;失败返回明确错误。
#### 2.2 Credential schema(磁盘 + 类型)
**磁盘 frontmatter 示例**`relativePath` **一律相对 vault root**,禁止 `..`):
```markdown
---
mnote.kind: credential
schema: mnote.vault.credential.v1
id: cred_01HZX...
title: GitHub — work
url: https://github.com/login
username: alice
# 示例为模板字面量(非真实密钥):Li@[A]s3cret
password: <example-template>
passwordHint: "Li@[A]***"
email: alice@example.com
apikey: ""
token: ""
tags: [work, github]
fields:
otp_issuer: GitHub
attachments:
- refId: att_cookies_1
kind: cookies
relativePath: attachments/cred_01HZX.../cookies-a1b2.txt
label: login-cookies
createdAt: 2026-07-19T08:00:00Z
updatedAt: 2026-07-19T09:30:00Z
createdBy: user_xxx
updatedBy: user_xxx
---
## Notes
登录备注等自由文本(P0 按 L0 返回;勿把真密码只放 notes)。
```
**磁盘 secret 字段**`password` / `apikey` / `token`**明文 string**(一期)。
**API 投影 `SecretFieldState`wire**
```ts
// 仅出现在 API 响应,永不作为磁盘存储类型
type SecretFieldState =
| { state: "absent" } // 磁盘无值或空
| { state: "masked" } // 有值但不返回明文
| { state: "revealed"; value: string; expiresAt?: string }; // 仅 reveal/resolve 短时
```
**存储侧 Rustmnote-web vault_storeP0**
```rust
pub struct VaultCredentialRecord {
pub id: String,
pub title: String,
pub url: Option<String>,
pub username: Option<String>,
pub password: Option<String>, // 磁盘明文;序列化到 API 时变 SecretFieldState
pub password_hint: Option<String>, // L0 可展示;禁止等于 password
pub email: Option<String>,
pub apikey: Option<String>,
pub token: Option<String>,
pub tags: Vec<String>,
/// P0: **仅非密钥元数据**(如 otp_issuer、environment)。
/// 禁止放入 recovery_code / otp_secret 等;此类应使用 token/apikey 或后续 secretFields。
pub fields: BTreeMap<String, String>,
pub attachments: Vec<VaultAttachmentRef>,
pub notes_markdown: String,
pub status: VaultItemStatus, // active | deleted
pub created_at: String,
pub updated_at: String,
pub relative_path: String, // entries/{id}.md 或 trash/entries/{id}.md
}
pub struct VaultAttachmentRef {
pub ref_id: String,
pub kind: String, // cookies | session | other
pub relative_path: String, // vault-root-relativenormalize 后不得含 ..
pub label: String,
pub content_type: Option<String>,
}
```
**`fields` P0 合同**
- 仅 L0 非密钥元数据。
- 服务端 create/update:若 key 命中拒绝列表(`password`/`token`/`apikey`/`secret`/`otp_secret`/`recovery` 等,大小写不敏感)→ `400 vault_fields_secret_forbidden`,提示改用标准 secret 字段。
- 不提供对 `fields` 值的 reveal/resolve;避免旁路。
**`passwordHint`**
- L0;可进 list index。
- create/update:若 `passwordHint``password` 在 trim 后相等 → **一律** `400 vault_hint_equals_secret`
**禁止** auto-clear / warn-and-continue 分支(P0 无第二路径)。
**必填**
- `title` 非空。
- create:建议至少 `url|username|password` 之一;UI 引导,服务端可 warning 但仍允许仅 title(产品可收紧)。
#### 2.3 `vault-index.json`
```json
{
"schema": "mnote.vault.index.v1",
"revision": 12,
"updatedAt": "2026-07-19T09:30:00Z",
"entries": {
"cred_01HZX...": {
"id": "cred_01HZX...",
"title": "GitHub — work",
"url": "https://github.com/login",
"username": "alice",
"email": "alice@example.com",
"passwordHint": "Li@[A]***",
"tags": ["work", "github"],
"hasPassword": true,
"hasApikey": false,
"hasToken": false,
"attachmentCount": 1,
"relativePath": "entries/cred_01HZX....md",
"updatedAt": "2026-07-19T09:30:00Z",
"status": "active"
}
}
}
```
规则:
- create/update/delete/restore/purge **原子**更新 indextemp + rename)。
- index **永不**存 password/apikey/token 明文。
- **Rebuild**index 损坏或不一致时):
1. 扫描 `entries/*.md``trash/entries/*.md`
2. **L0-only allowlist 解析**:只抽取 id/title/url/username/email/passwordHint/tags/has* 标志/attachments 计数/status**丢弃** password/apikey/token 键,不得进入 debug map。
3. 解析错误日志只含 `credentialId` / 相对路径 / error code**禁止** frontmatter 原文或 secret 子串。
4. 运维:删除损坏 index 后调用 `POST /api/vault/ensure``POST /api/vault/reindex`P0 可合并进 list 自动 rebuild)→ 重写 index。
#### 2.4 密码槽展示约定
| 层 | 约定 |
|----|------|
| 存储 | `password` 完整可用字符串(明文) |
| L0 提示 | 可选 `passwordHint`(不可逆示意,≠ 真密码) |
| UI | 默认 `••••••••`reveal 全显;复制走 reveal 或 copy-after-reveal |
| 未来 | 加密字段延期 |
页眉文案:**本地结构化密码箱,非加密保险箱**。
#### 2.5 附件(cookies / session)— P1 起完整交付
- **模型**:受 5-34 **启发**,但 owner 是 **credential**,不是页面 `.md`;不是同一 `AttachmentRef` 解析链。
- **路径**`relativePath` 仅 vault-root-relative,如 `attachments/{credentialId}/cookies-{id}.txt`reject `..` 与绝对路径。
- **下载**`GET /api/vault/attachments/{credentialId}/{refId}?rootUri=`(需 `vault.view`);**禁止** 用 `/api/local-folder/files/open`
- **上传**`POST /api/vault/attachments` 或 command `vault.attachment.put``vault.edit`)。
- ensure 不要求预建每个 credential 子目录;put 时 create。
#### 2.6 删除策略 — **P0 冻结:软删除(方案 B)**
| 操作 | P0 行为 |
|------|---------|
| `vault.delete` | 条目 md + 其 attachments **移动**到 `.mnote/vault/trash/...`index `status: deleted` |
| `vault.list?status=active`(默认) | 仅 active |
| `vault.list?status=deleted` | 回收站 |
| `vault.restore` | 移回 `entries/` / `attachments/`status active |
| `vault.purge` | 硬删 trash 内文件;index 移除键 |
| 全局 `.mnote/trash` | **不使用** |
P0 UI 必须有回收站 tab(非「以后再说」)。
**不做** P0 硬删作为默认 delete(避免与 K6 及回收体验不一致)。
---
### 3. mnote-web API(非 tree kernel
#### 3.1 命令/查询清单
| 名称 | HTTP | 权限(P0 矩阵 §5.1 | 阶段 |
|------|------|----------------------|------|
| `vault.ensure` | `POST /api/vault/ensure` | workspace 可读+可建目录 | P0 |
| `vault.reindex` | `POST /api/vault/reindex` | view | P0 |
| `vault.list` | `GET /api/vault/list` | view | P0 |
| `vault.get` | `GET /api/vault/items/{id}` | view | P0 |
| `vault.create` | `POST /api/vault/items` | edit | P0 |
| `vault.update` | `PATCH /api/vault/items/{id}` | edit | P0 |
| `vault.delete` | `DELETE /api/vault/items/{id}` | edit | P0 soft |
| `vault.restore` | `POST /api/vault/items/{id}/restore` | edit | P0 |
| `vault.purge` | `POST /api/vault/items/{id}/purge` | edit | P0 |
| `vault.reveal` | `POST /api/vault/items/{id}/reveal` | view + 登录会话 | P0 |
| `vault.cipherBook.list` | `GET /api/vault/cipher-book` | view | P0 |
| `vault.cipherBook.put` | `PUT /api/vault/cipher-book/{key}` | edit | P0 |
| `vault.cipherBook.delete` | `DELETE /api/vault/cipher-book/{key}` | edit | P0 |
| `vault.cipherBook.reveal` | `POST /api/vault/cipher-book/{key}/reveal` | view + 登录会话 | P0 |
| `vault.resolve` | `POST /api/vault/items/{id}/resolve` | resolve capability | P1 |
| `vault.import.preview/apply` | `POST /api/vault/import/*` | edit | M |
| `vault.attachment.*` | 见下 | view/edit | P1P2 |
| `vault.export` | — | export | P2 |
#### 3.1a 密文簿 / 分组 / 同站多账号(P0 产品补充)
1. **密文簿(cipher-book**
- 磁盘:`{workspaceRoot}/.mnote/vault/cipher-book.json`schema `mnote.vault.cipherBook.v1`)。
- 密码 / apikey / token 可写模板,如 `Li@[A]s3cret``[Key]` 中 Key 为 `[A-Za-z0-9_]{1,32}`
- **存储**:条目里保留模板原文;**reveal** 时展开为明文 `value`,并可选返回 `template` / `usedCipherKeys` / `missingCipherKeys`
- L0 list 仅返回 keys + `hasValue`**永不**投影片段明文。
2. **同站多账号**
- **不**对 `url` 做唯一约束;同一 URL 可对应多条 credential(不同 username)。
- UI:详情提供「同站新账号」预填 url/folderPath;列表 meta 展示 username 区分。
3. **分组折叠(folderPath**
- 逻辑字段 `folderPath`(如 `个人/银行/招商`),**不是** vault 下真实子目录。
- 左栏按路径多级折叠;折叠状态可存 sessionStorage。
- 条目文件仍平铺在 `entries/{id}.md`
- **表单 UX(已落地)**:下拉选择已有 `folderPath` + 文本直接输入新路径(无弹窗、无空组、无「在此新建」);组随条目出现。
4. **同站 URL 折叠**
- 同一 `folderPath` 节点下,**相同非空 url 且 ≥2 条** 时,再包一层可折叠「站点组」。
- 单条 URL 不额外包组;无 url 条目平铺。
5. **共享到 AI 密码本**
- `POST /api/vault/items/{id}/share-to-ai`:把条目(含 secret 模板)**复制/更新**到目标 actor 的 managed 默认工作区 vault。
- 默认目标 actor`mnote-e2e`env `MNOTE_AI_VAULT_ACTOR` 可改)。
- 副本加 tag `ai-shared``folderPath` 默认 `from-{sourceActor}`
- **双向链接(磁盘 frontmatter**:源 `sharedToAi{targetActorId,targetId,sharedAt,lastSyncedAt}`;副本 `sharedFrom{sourceActorId,sourceId,sharedAt}`
- 再次共享同一源:**更新**已有 AI 副本(不重复建条);按钮文案变为「同步到 AI 副本」。
- **编辑自动同步**`PATCH` 源条目默认 `syncAiShare=true`,若有 `sharedToAi` 则推送到 AI 副本(密文 key 同步规则同 share)。
- **AI 密码本识别**list 返回 `isAiVault` / `vaultRole`;AI 本顶栏标识「AI 密码本」,**隐藏**「共享到 AI」;副本展示「AI 副本」横幅/标签。
- 用户本上已共享条目:标题旁轻量 chip「已共享」;按钮「共享 AI / 同步 AI」+「撤销共享」。
- **撤销共享**`POST /api/vault/items/{id}/unshare-from-ai`:清除源 `sharedToAi`AI 副本 soft-delete(进回收站);密文 key 不删。幂等。
- **密文簿同步(默认开)**`syncCipherKeys` 默认 `true`
- 密文簿 **按 workspace 隔离**;单本内 key 唯一。
- 编辑 secret 明文;查看态掩码;空字段默认不展示。
- Skill`skills/mnote-vault` + `CAPABILITY_PACKS` 注册 id `mnote-vault`hermes/reasonix 可发现)。
#### 3.2 HTTP 映射(完整 P0 + 附件骨架)
```text
POST /api/vault/ensure
POST /api/vault/reindex
GET /api/vault/list?rootUri=&sourceKind=local_folder&status=active|deleted
GET /api/vault/items/{id}?rootUri=
POST /api/vault/items
PATCH /api/vault/items/{id}
DELETE /api/vault/items/{id}
POST /api/vault/items/{id}/restore
POST /api/vault/items/{id}/purge
POST /api/vault/items/{id}/reveal # body: { "field": "password"|"apikey"|"token" }; expands [Key]
POST /api/vault/items/{id}/share-to-ai # body: { rootUri, targetActorId? } → AI vault clone
POST /api/vault/items/{id}/unshare-from-ai # body: { rootUri, targetActorId? } → clear link + soft-delete AI copy
GET /api/vault/cipher-book?rootUri=
PUT /api/vault/cipher-book/{key} # body: { rootUri, value }
DELETE /api/vault/cipher-book/{key}?rootUri=
POST /api/vault/cipher-book/{key}/reveal
POST /api/vault/items/{id}/resolve # P1
POST /api/vault/import/preview
POST /api/vault/import/apply
GET /api/vault/attachments/{credentialId}/{refId} # P1
POST /api/vault/attachments # P1
DELETE /api/vault/attachments/{credentialId}/{refId} # P1
```
统一响应壳:`{ ok, requestId, traceId, owner: "mnote-web", result }`
错误码:`vault_root_required` / `vault_forbidden` / `vault_item_not_found` / `vault_path_denied` / `vault_secret_denied` / `vault_fields_secret_forbidden` / `vault_hint_equals_secret` / `vault_revision_conflict` / `vault_mask_sentinel_rejected`
#### 3.3 Projection
`mnote.vault.list.v1` / `mnote.vault.item.v1`secret 字段仅为 `SecretFieldState`(默认 masked/absent)。
`mnote.vault.secret.v1`reveal/resolve 用;**access log 不得记录 `value`**。
#### 3.4 实现落点(owner crate
| 层 | 落点 |
|----|------|
| HTTP + storage | `mnote-web/src/routes/vault.rs` + `vault_store.rs`(或同模块) |
| 共享 path deny | `mnote-web``vault_path.rs``is_vault_sensitive_relative_path(rel) -> bool` |
| 调用方 | open/stat/upload/write、`page_ai_pi``resolve_file_path` / local_file_read|patch |
| SSR | `ssr/pages/vault.rs` + gateway/page route |
| Browser | `browser/vault-workbench-runtime.js`;在 `layout.rs` 与其它 workbench 一样加 **script 标签**,并由 `mnote-ui-runtime.js` import 注册(与 filetree/trash 模块同一模式) |
| core-protocol | **可选** P0:仅 schema 字符串常量;不强制 |
| bridge-runtime | **P0 不注册** |
原则:浏览器 **禁止** 直接写 vault 路径;唯一 writer = vault HTTP handlers。
#### 3.5 Secret 字段 create / update 语义(**强制**
**标准 secret 键**`password` | `apikey` | `token`
**PATCH `vault.update` bodycamelCase**
| 客户端传入 | 服务端行为 |
|------------|------------|
| 字段 **缺省**JSON 键不存在) | **保持**磁盘原 secret |
| `"password": null``{ "password": { "clear": true } }` | **清空** secret |
| 顶层 `"password": ""`(空字符串)或 `{ "set": "" }` | **清空** secret(与 clear 同义;**禁止**把空串当「未改」) |
| `"password": { "set": "<new>" }` | 设为新明文(内部空格保留;仅拒纯 mask 哨兵) |
| `"password": "••••"` / `"****"` / `"•"+` / `"••••••••"` 等掩码哨兵 | **`400 vault_mask_sentinel_rejected`**,不写盘 |
| 顶层 `"password": "plaintext"`(非空裸字符串) | **允许**,语义同 `{ set }`(便于表单);仍拒绝哨兵 |
**仅改 title/tags 等 L0 字段**body 不含 secret 键 → secrets **原样保留**。单测:**update title only keeps password**。
**create**
- 可用裸字符串或 `{ set }`
- 缺省 secret → absent。
- 禁止哨兵。
**import apply**:写入真实解析值;不经 mask 状态机。
**乐观并发(P0 建议)**
- index / item 带 `revision`update 可带 `baseRevision`;冲突 → `409 vault_revision_conflict`(两 tab 编辑)。P0 最低:last-write-wins + UI 保存后 re-fetchchecklist 优先有 revision 更好。
---
### 4. 前端落点
#### 4.1 模块地图
| 层 | 文件 | 职责 |
|----|------|------|
| SSR | `ssr/pages/vault.rs` | `VaultPage``current_nav="vault"` |
| Page route | `gateway``routes/vault_page.rs` | auth + rootUri + bootstrap list(无 secret |
| API | `routes/vault.rs` | CRUD + reveal + soft-delete |
| Path deny | `vault_path.rs` + 挂入 open/stat/… | §5.2 |
| Styles | `ssr/styles` 或 vault CSS | workbench |
| Runtime | `browser/vault-workbench-runtime.js` | CRUD UI |
| 入口 | `layout.rs` | 密码箱链接 + script 注入 |
| 拦截 | resource-open **附加** UX | **不能**替代 server deny |
#### 4.2 刷新模型(修正原 watcher 误称)
| 来源 | P0 |
|------|-----|
| vault API 成功 | 本地 patch list 或 re-fetch `vault.list` |
| 工具栏「刷新」 | re-fetch |
| 现有 local folder watcher / WS tree events | **不**用于 vault UI`.mnote` 变更本就不按产品树消费 |
| 外部编辑器改 vault 文件 | **非目标**;用户按「刷新」。未来若做 `vault.changed`,只由 vault command 广播,**永不**把 secret body 送进 search 索引管线 |
| setInterval | **禁止** |
#### 4.3 禁止
- tiptap 表单、localStorage 存 password、UI 第二套真源(draft 仅编辑态)。
---
### 5. 安全分级与硬边界
#### 5.0 L0 / L1 / L2
| 级 | 内容 | 通道 |
|----|------|------|
| L0 | title url username email tags passwordHint has* attachmentCount updatedAt notes(全文) fields(非密钥) | list/get 默认 |
| L1 | password/apikey/token 明文 | `vault.reveal`;**登录会话 + 审计**(**不**宣称 OS 级用户手势/CSRF≠手势) |
| L2 | 同上供 AI | `vault.resolve`;默认 **deny**(§5.1);瞬时;transcript 脱敏 |
#### 5.1 P0 鉴权矩阵(冻结;capabilities_json 延后)
P0 **不**解析 `DirectoryGrant.capabilities` 中的 `vault.*`(今日无写入路径与 enforcement helper)。映射:
| 主体 | vault.view | vault.edit | vault.resolve | vault.export |
|------|------------|------------|---------------|--------------|
| local_folder **owner**manifest owner / 等价) | ✅ | ✅ | ❌(P1 显式开) | ❌ |
| directory grant **write** | ✅ | ✅ | ❌ | ❌ |
| directory grant **read-only** | ❌ | ❌ | ❌ | ❌ |
| share link read | ❌ | ❌ | ❌ | ❌ |
| anonymous | ❌ | ❌ | ❌ | ❌ |
说明:
- **协作方默认不可见 vault**(含 read grant)。与「共享目录可读普通文件」分离:vault 是系统敏感空间。Open Q 若产品要 read grant ⇒ view,属 **改矩阵** 的产品决策,P0 实现按上表。
- `vault.resolve` 默认 **deny**P1 在 AI policy / 显式设置中改为 `ask``allow`。L2 文案统一为 deny-by-default,不再写「生产建议 ask」作为默认。
- revealauthenticated session + audit,非生物/OS 手势。
**P1 capabilities**:再在 grant / ai_policy 写入 `vault.view|edit|resolve|export` 并做 enforcement helper;不阻塞 P0。
#### 5.2 通用路径 Deny — **P0 合入门(全表面清单)**
**共享谓词**(单一实现,禁止复制粘贴分叉):
```rust
/// rel: workspace-root-relative, `/` 分隔, 已 strip 前导 ./ , 拒绝 .. 段之后再判断
fn is_vault_sensitive_relative_path(rel: &str) -> bool {
let n = normalize_rel(rel); // 大小写敏感 FS 按原样;比较用统一 trim
n == ".mnote/vault" || n.starts_with(".mnote/vault/")
}
```
命中 → **403**code `vault_path_denied`body **无** 文件内容(resource/read 亦不得含 `"text"` 字段中的 secret)。
**首选 choke pointPR-S 实现策略,减少漏挂)**
| 共享 resolve | 今日调用方(已核实) | 动作 |
|--------------|----------------------|------|
| `resolve_local_open_path``local_folder_source.rs` | `open_local_file`;经 `resolve_local_file_open_path``read_local_resource` / `write_local_resource` | **在此**调用 `is_vault_sensitive_relative_path`,一次覆盖 open + resource read/write |
| `resolve_local_file_open_path` | 内部调 `resolve_local_open_path` | 继承上列;勿再复制 matcher |
| Pi `resolve_file_path` / `resolve_root_relative_path``page_ai_pi/runtime.rs` | `local_file_read` / `local_file_patch` | **在此** denyleaf tool 不各自拼规则 |
**注意**`stat_local_file` **不**走 `resolve_local_open_path`(自建 `canonical_root.join(path)`),必须在 stat handler **显式** deny,不能假设 choke 自动覆盖。
**P0 必须挂接的表面(实现前打勾;编号冻结)**
| # | 表面 | 符号 / 路由(已核实) | 动作 |
|---|------|----------------------|------|
| D1 | `GET /api/local-folder/files/open` | `open_local_file``resolve_local_open_path` | denychoke |
| D2 | `GET /api/local-folder/files/stat` | `stat_local_file`**独立** path join | **显式** deny |
| D3 | `GET /api/local-folder/resource/read` | `read_local_resource``resolve_local_file_open_path`;返回 JSON **`text` 全文** | denychoke);**合入门必测** |
| D4 | `POST /api/local-folder/resource/write` | `write_local_resource``resolve_local_file_open_path` | denychoke |
| D5 | `POST /api/local-folder/assets/upload` | `upload_local_markdown_asset``write_local_folder_file_upload` / `write_local_markdown_asset``filetree.folder.drop``targetRelativePath` | 目标 rel 落 vault → deny |
| D6 | 其它 local 写路径:page body / mindmap / mkdir·rename·move·archive 若接受 root-relative 路径 | 至少:`write_local_markdown_page_body``write_local_mindmap_data`tree/local executor 中 rename/move/archive 入口 | 源或目标 vault → denyvault 只走 vault API |
| D7 | `mnote.local_file.read` | `page_ai_pi` `local_file_read` | deny |
| D8 | `mnote.local_file.patch` | `local_file_patch` | deny |
| D9 | Pi path resolve | `resolve_file_path`(及 root+rel→abs helper | 敏感 rel → Err,不返回内容 |
| D10 | tree command / resource archive 指向 vault | local tree executor / archive helpers | deny 或明确 no-op 错误(若入口最终调 D1/D6 resolve,可标「由 shared resolve 覆盖」并单测一条) |
| D11 | OnlyOffice / media 代理 | `onlyoffice.rs``fileUrl`**`/api/local-folder/files/open`** | **由 D1 choke 覆盖**;勿再开平行 open |
| D12 | SSR / document `local-md:` | document / gateway 解码 `local-md:` 相对路径后读盘 | path 在 vault → 不渲染正文;可选 redirect `/vault` |
**Live `/api/local-folder/*` 路由盘点**`routes/mod.rs` 已核实,避免 D 表再漏一等公民 content 面):
| 路由 | Handler | vault path deny |
|------|---------|-------------------|
| `GET .../files/open` | `open_local_file` | **D1**choke |
| `GET .../files/stat` | `stat_local_file` | **D2**(显式) |
| `GET .../resource/read` | `read_local_resource` → JSON **`text` 全文** | **D3**choke,合入门) |
| `POST .../resource/write` | `write_local_resource` | **D4**choke |
| `POST .../assets/upload` | `upload_local_markdown_asset`(含 `filetree.folder.drop` + `targetRelativePath` | **D5** |
| `GET .../events` | local-folder event bus / watcher SSE | **N/A content**:事件不回文件正文;watcher 已滤 `.mnote`(§4.2 non-goal |
| `POST .../shared-cache/record` | `record_shared_cache` | **N/A**:写 `.mnote/share-cache.json` 元数据,不读任意 rel 正文 |
| `POST .../sync/pending-change` | `record_sync_pending_change` | **N/A**sync 元数据 |
| `POST .../sync/conflict-report` | `write_sync_conflict_report` | **N/A**:报告写 `.mnote/sync-reports` |
| `GET .../workspaces/default` | `create_default_local_workspace` 等 | **N/A**workspace bootstrap,无任意 path 读盘 |
| `ANY .../ocr/{jobs,status,read,insert,delete}` | `retired_local_ocr_endpoint` | **已退役**(指向 LightRAG);P0 **不**新增 OCR vault 路径;若未来复活须走 `is_vault_sensitive_relative_path` |
| (非 local-folder 但相关)`GET /api/tree/local-folder-watch` | legacy / 侧车 | 以 tree live 为准;**不**作为 vault content read 面 |
**结论**:今日能回任意 workspace-relative **文件字节/全文** 的 HTTP 面仅 **open / stat / resource/read / resource/write / assets/upload**——均已进 D1D5。其它 local-folder 路由不构成 secret 正文旁路。Pi / SSR / OnlyOffice / tree write 见 D6D12。
**单元测试(每个 D1–D5、D7–D9 至少一条;D3 为合入门)**
- D1 open / D2 stat / **D3 resource/read**`.mnote/vault/entries/x.md` → 403 `vault_path_denied`;响应体无 fixture password、`password:` 样例串;**D3 无 `text` 明文**。
- D4 resource/write / D5 assets/upload 目标 vault → 403。
- D7D9`mnote.local_file.read` / patch / resolve → deny,无 content。
**原生 Pi / 非 MNote 文件工具残留风险**
- 强制:凡经 MNote tool bridge 的路径检查必须 deny。
- 若 Pi 进程 cwd/sandbox 含 workspace root 且存在 **绕过 MNote** 的原生 read:属残留风险。P0 文档披露;缓解:Pi sandbox 不把 `.mnote/vault` 列入可写/可读 allowlist(若 runtime 支持 path deny 列表则配置);无法 100% 时在 AI 设置提示「请勿将 vault 目录加入 agent 原生工作区」。
- 不把 vault 路径写入 `AiAccessScope.allowed_file_paths` / `allowed_roots` 的子路径 allow。
**顺序**:**PR-S(安全锁)在任意默认可写 secret 的 API 对非测试环境生效之前 merge**(见 PR Plan)。Feature flag 默认 **关** 直至 **P0-S** 全绿(含 D1D5、D7D9;尤其 D3 resource/read)。
#### 5.3 威胁模型(摘要)
| 威胁 | 严重度 | 缓解 |
|------|--------|------|
| HTTP open/stat 读 vault | 高 | D1D2 + 测试合入门 |
| HTTP **resource/read** 回全文 | 高 | **D3** + choke `resolve_local_open_path` |
| resource/write · assets/upload 写 vault | 高 | D4D5 |
| Agent local_file 读 vault | 高 | D7D9 |
| RAG 索引 vault | 高 | exclude + 单测 |
| list/get 回明文 | 高 | mask + reveal 分端 |
| 日志打印 secret | 高 | reveal/resolve/rebuild 错误路径禁 value |
| 双轨 `个人/密码/` | 高 | §7.4 警告 + 可选 exclude |
| 磁盘物理读取 | 高(接受) | 非加密声明 |
| 掩码写回 | 中 | sentinel reject |
| XSS 读 DOM | 中 | 最短明文展示 |
---
### 6. AI 分阶段
#### P0
-`mnote.vault.*` 工具暴露。
- D7D9(及 HTTP D1D5,含 **resource/read**deny 生效;AI / 通用 HTTP 读 vault 失败且无 body。
#### P1 — **多 agent 唯一策略(2026-07-20 冻结并落地)**
**策略名:单 AI 密码本 + 单 resolve 通道 + 多 agent 只消费 AI 本。**
| 项 | 冻结 |
|----|------|
| 凭证池 | 仅 AI 密码本(`MNOTE_AI_VAULT_ACTOR`,默认 mnote-e2e |
| 读密 | **仅** `mnote.vault.resolve` / `POST /api/vault/ai/items/{id}/resolve` / CLI |
| 禁止 | 通用 file / local_file / open / resource / RAG 读 `.mnote/vault/**` |
| 用户 vault | 人用 reveal**resolve 禁止**`vault_resolve_ai_only` |
| 长期 | AI 本内条目即长期授权;撤权=删/软删 AI 本条目 |
| 快速 | 本机 HTTP/CLI,不经浏览器 |
| 策略 SSOT | `/home/lix/.agent-infra/vault-policy.md` + `$mnote-vault` + Paseo `appendSystemPrompt` |
- **已落地**`list_ai` / `get_ai` / `resolve` APIPi `mnote.vault.list|get|resolve`(默认 allowplan 模式 resolve deny);receipt 脱敏 value`scripts/mnote-vault-cli.js`
- transcriptagent 应使用 `transcriptHint`(已解析 field),不把 value 贴进聊天。
- skills`skills/mnote-vault` + 全局 symlink + capability pack。
#### P2
- 协助录入确认流、attachment cookies、自注册回写、export;按 credential/run grant(可选)。
---
### 7. 迁移(`个人/密码/`
#### 7.1 源形态
- 惯例路径:`{workspaceRoot}/个人/密码/**/*.md`(**用户环境观察**,数量因工作区而异)。
- 自由 md;导入前 **仍在** FileTree/RAG/search 范围内。
#### 7.2 向导
preview → 用户勾选 → apply;冲突 skip/副本;**禁止静默覆盖**;默认不删源。
#### 7.3 启发式
frontmatter 优先;否则中英关键行;title=文件名 stem;低置信人工确认 password。
#### 7.4 双轨残留风险(正式产品态度)
正式 vault **不能**单独声称「工作区密码已卫生」。在源目录仍存在期间:
1. Empty state / 导入页 **固定警告**:「`个人/密码/` 等目录仍可能被搜索与知识库索引;请导入后归档。」
2. 推荐 apply 后可选移动源到 `个人/密码/_imported/`,并将 `_imported` 或整树加入 knowledge/local-search **excludePatterns**(实现可复用已有 exclude 设置)。
3. **可选**Phase M+):内置 exclude 候选 `个人/密码/**`(默认 **不** 静默全局排除,避免误伤同名笔记;UI 一键启用)。
4. 设计稿与验收 **不** 把「端到端密钥卫生」标为 P0 done,除非源已导入或 exclude。
---
### 8. Observability
**P0 唯一审计 sink**`{workspaceRoot}/.mnote/vault/audit.jsonl`append-only)。
示例行:
```json
{"ts":"2026-07-19T09:30:00Z","action":"reveal","actorId":"user_x","credentialId":"cred_…","field":"password","requestId":"…","ok":true}
```
禁止字段:`value` / `password` / `body` / frontmatter 原文。
**Rebuild / 解析错误**:同样只写 id/path/code。
可选 metricslist 延迟、reveal 计数、deny 计数。
**运维**index 损坏 → `POST /api/vault/reindex` 或 list 自动 rebuild;说明写入 support runbook 一节(本设计 §2.3)。
---
### 9. Rollout
| 项 | 策略 |
|----|------|
| Feature flag | `MNOTE_VAULT`:**默认 0(关)**。允许默认开的门闩:**P0-S 全绿**(含 D1D5、D7D9 单测,**尤其 D3 resource/read**+ 建议 **P0-E smoke** 绿后再按环境打开。**无** `P0-D` 阶段名 |
| 入口 | flag 关:隐藏 quick-action、API 404 |
| 密钥写入 | **禁止** 在 flag 默认开且无 path denyP0-S)时合入 |
| 回滚 | flag 关 + 隐藏入口;**保留** `.mnote/vault` 数据 |
| 迁移 | 不自动 import;导入前建议用户备份源目录 |
---
### 10. Alternatives Considered
| 方案 | 描述 | 优点 | 缺点 | 结论 |
|------|------|------|------|------|
| A. 普通 md + FileTree | 继续 `个人/密码/` | 零开发 | 无分级/RAG 风险 | 否决 |
| B. Control-plane DB 存密 | Turso 表 | 易审计 | 违背 local-first 正文真相 | 否决 |
| C. `.mnote/vault` + 专用页 + mnote-web API | 本文 | 对齐 trash、可 deny、可分级 | 新模块 | **采用** |
| D. Modal 如 trash | 不改 URL | 上下文保持 | 用户要专门页;表单空间不足 | 否决 IA |
| E. 一期加密 | master password | 真保密 | 工期/UX/AI resolve | 延期 |
| F. 单文件 SQLite/JSONL vault | `.mnote/vault/vault.db` | 索引快、无 per-file secret 扫描 | 人读救援差、与 md 生态/git diff 差 | **否决 P0**(人读救援优先,K4 |
| G. 先做 trash 式 modal 再升级全页 | 增量 UI | 更快出壳 | 与已确认 IA 冲突、返工 | 否决 |
---
## Security & Privacy
见 §5。补充:Clipboard 清空 best-effortSSR 首屏禁止 secretpath canonicalize + vault prefix deny。
---
## Open Questions
1. 图标 `lock` vs `key`?建议 lock。
2. 是否永久废弃「文件总览」产品名?当前 `/files` 仅 redirect。
3. **已冻结 P0**read grant **无** vault.view;若产品要协作者只读 vault → 改 §5.1 矩阵另开变更。
4. **已冻结**`vault.resolve` 默认 **deny**
5. 导入候选路径是否扩展 `密码/``Passwords/`?默认仅 `个人/密码`
6. credentialId:建议 ULID/UUID。
7. empty state 自动 ensure?建议是。
---
## Key Decisions
| # | 决策 | 理由 |
|---|------|------|
| K1 | 存储 `{workspaceRoot}/.mnote/vault/` | 系统空间;FileTree/RAG 已忽略 `.mnote` |
| K2 | 专用 `/vault` 全页 CRUD,非 modal/tiptap | 用户明确要求 |
| K3 | 死链 `/files`/`inventory_2` → 密码箱 | 固定入口 |
| K4 | per-entry frontmatter md + L0 index | 人读救援、git 友好;否决纯 SQLite P0 |
| K5 | index 只存 L0rebuild allowlist 解析 | 防 secret 进索引/日志 |
| K6 | **P0 软删除** + 工作台回收站;不进全局 trash | 降泄露;与 restore/purge API 同阶段 |
| K7 | L0/L1/L2reveal=会话+审计 | 最小暴露 |
| K8 | 通用文件面 **server deny** vaultAI 仅 `mnote.vault.*`P1 | 堵住 open/stat/local_file 实锤缺口 |
| K9 | 一期不加密 at-rest | 工期与诚实威胁模型 |
| K10 | 迁移可选、不静默覆盖;双轨风险明示 | 保护旧数据与预期 |
| K11 | 刷新=command 结果 + 手动刷新;**非** watcher;禁 setInterval | 符合现网 watcher 行为 |
| K12 | 大类 `12-vault`**非** 04-tree 树 bug 默认 owner | 系统空间产品 |
| K13 | **P0 owner = mnote-web 系统空间 API**;非 tree kernel / 非 P0 bridge command | 对齐 trash;避免假 kernel |
| K14 | Secret PATCH:省略=保留;clear/null=清空;set=写入;拒掩码哨兵 | 防擦密与存 `••••` |
| K15 | Feature flag 默认 **关****P0-S**(含 D3 resource/read)绿前不默认可写 secret | 安全顺序;无 P0-D 阶段 |
| K16 | P0 审计 = `.mnote/vault/audit.jsonl` only | 可执行、无双 sink 歧义 |
| K17 | P0 鉴权矩阵:owner/write⇒view+editread/share⇒无 vaultresolve 默认 deny | 可实现;capabilities 延后 P1 |
| K18 | `fields` 仅非密钥 L0;标准 secret 三字段 + passwordHint L0**hint==password → 仅 400 reject** | 防旁路;合同无 auto-clear |
| K19 | 附件 path 相对 vault root;专用 GET;禁 files/open | 防 cookies 绕过 |
| K20 | path deny 优先挂 `resolve_local_open_path` / Pi resolve**D3 resource/read** 一等公民 | 防 leaf-only 漏挂 |
---
## 执行清单(Checklist
### Phase 0 — 合同
- [ ] 迁入 `design/12-vault/process/`;更新 `design/README.md` 主线(注明 12-vault 为系统空间,非 tree-domain
- [ ] 冻结 schema / K1K20 / §5.1 矩阵 / §3.5 PATCH
- [ ] 冻结 deny 表面 **D1D12**`is_vault_sensitive_relative_path`choke = `resolve_local_open_path` + Pi resolve + stat 显式
### Phase P0-S — 安全锁(**先于或与首个 secret 写入同 PR 合并门**
- [ ] 实现 `is_vault_sensitive_relative_path` + 单测
- [ ] **choke**`resolve_local_open_path` 内 deny(覆盖 D1 open、D3 resource/read、D4 resource/write
- [ ] D2 `stat_local_file` **显式** deny(不走 resolve
- [ ] D1 open / D2 stat / **D3 `GET .../resource/read`** → 403 `vault_path_denied`D3 响应无 `"text"` 明文 / fixture password
- [ ] D4 resource/write / D5 assets/upload 目标 vault → 403
- [ ] D6 至少一条 page-body 或 tree 写路径 deny 单测
- [ ] D7D9 local_file read/patch + resolve_file_path deny
- [ ] knowledge_ragvault 路径不可 ingest(单测)
- [ ] local_searchvault 不进 index;用户 includePaths 强指 vault → 拒绝
- [ ] FileTree 不可见(`.mnote` + 断言)
- [ ] `MNOTE_VAULT` 默认 0 时行为符合 §9(门闩引用 **P0-S / P0-E**,无 P0-D
### Phase P0-A — 存储
- [ ] ensure 含 vault + trash 子目录
- [ ] vault_storeentry 读写、index 原子更新、L0 allowlist rebuild
- [ ] soft-delete 移动文件 + status
- [ ] restore / purge
- [ ] audit.jsonl appendreveal/delete/…)
- [ ] parse error 日志无 password 单测
- [ ] `cargo test -p mnote-web`:模块名建议 `vault_store` / `vault_path` / `vault_api`(固定,不用「或等价」含糊)
### Phase P0-B — HTTP
- [ ] 注册 §3.2 全部 P0 路由
- [ ] get/list maskreveal 审计无 value
- [ ] update title-only keeps password 单测
- [ ] mask sentinel → 400
- [ ] fields 密钥键 → 400
- [ ] passwordHint == password → 400 `vault_hint_equals_secret`(无 auto-clear
- [ ] flag 关 → 404
### Phase P0-C — SSR + 入口 + UI
- [ ] `VaultPage` + `mnote-vault-workbench`
- [ ] bootstrap list **无** password 明文;HTML 单测/fixture 断言
- [ ] layout 密码箱入口 + **明确** script`layout.rs` 增加 `vault-workbench-runtime.js` module script(同侧其它 browser runtime),并在 `mnote-ui-runtime.js` 侧 import 初始化
- [ ] deep-link `?id=`、dirty discard、回收站 tab、手动刷新
- [ ] 无 setInterval:对 `vault-workbench-runtime.js` 做静态扫描(`rg setInterval`
- [ ] `/files``/vault` 302
### Phase P0-E — Smoke
- [ ] `scripts/task-vault-crud-smoke.js`
- 登录 e2e 账号;开 rootensure
- create → list 见 L0 → get 无明文 password
- update title only → 磁盘 password 不变
- reveal → 得明文;audit.jsonl 有行且 `rg` 无 password 值
- delete → trash 路径存在;restorepurge 干净
- `GET .../files/open?path=.mnote/vault/entries/...` → 403
- `GET .../resource/read?path=.mnote/vault/entries/...` → 403body 无 fixture password / 无 secret `text`
- `GET .../files/stat?path=.mnote/vault/entries/...` → 403(或等价 deny,无敏感元数据滥用)
- SSR `/vault` HTML 不含 fixture password 字符串
- [ ] 禁止 sqlite3 直写 CP
### Phase M — 迁移
- [ ] preview/apply;冲突策略;双轨警告 UI
- [ ] 可选 archive + exclude 提示
- [ ] fixture 假密码导入 smoke3 条)
### Phase P1 — AI tools + capabilities
- [ ] `mnote.vault.*` 注册;resolve 默认 deny
- [ ] grant/policy 写入 vault.* 可选
- [ ] agent local_file.read vault deny smoke(回归)
- [ ] attachment GET/POST
### Phase P2 / 收口
- [ ] 录入/cookies/exportARCHITECTURE + AGENTS 一行;design → done
---
## Risks
| 风险 | 严重度 | 缓解 |
|------|--------|------|
| 误认已加密 | 高 | 页眉声明 |
| open/stat 合入漏挂 | 高 | P0-S 门禁 + 全表面表 |
| 双轨 `个人/密码/` | 高 | §7.4 |
| 导入误解析 | 中 | 低置信人工确认 |
| index 漂移 | 中 | reindex + 原子写 |
| 原生 Pi 绕过 MNote | 中 | 披露 + sandbox 建议 |
| revision 冲突丢更新 | 低–中 | re-fetch;可选 baseRevision |
---
## References
- ARCHITECTURE.md / AGENTS.md
- design 4-37 / 7-18 / 5-34 / 4-48
- 代码:`layout.rs``local_folder_source.rs`ensure/open/stat/**resource read·write**/upload/`resolve_local_open_path`)、`local_folder_watcher_registry.rs``gateway.rs` trash、`page_ai_pi/runtime.rs``core-protocol` AiAccessScope、`knowledge_rag.rs``local_search_index.rs`
---
## PR Plan
### PR-S — Vault path deny + index exclude locks**安全门,可先于或与 PR1 同列车**)
- **Title**`security(vault): deny .mnote/vault on open/stat/resource/local_file and lock RAG/search`
- **Files**`vault_path.rs`(新)、`local_folder_source.rs`**`resolve_local_open_path` choke**、`stat_local_file` 显式、upload/write helpers)、`page_ai_pi/runtime.rs`resolve_file_path、local_file_read|patch)、`knowledge_rag`/`local_search_index` 测试
- **Dependencies**:无
- **Description**:实现 **D1D12** 中的可触达面;**必须**含 D3 `resource/read` 单测。优先 choke 而非 leaf 复制。**无** vault 业务写入。合并门:P0-S 红线单测绿。
### PR1 — Vault store + ensure + soft-delete 文件语义
- **Title**`feat(vault): vault store, ensure dirs, soft-delete on disk`
- **Files**`local_folder_source` ensure、`vault_store.rs`、audit.jsonl、单元测试 `vault_store`
- **Dependencies**PR-S(或同 PR 内先 path deny
- **Description**:目录、md 合同、index、soft-delete/restore/purge 文件层;**无 HTTP** 或仅测内函数。
### PR2 — Vault HTTP APICRUD + reveal + restore/purge + mask PATCH
- **Title**`feat(vault): /api/vault CRUD, reveal, restore/purge with secret PATCH contract`
- **Files**`routes/vault.rs``routes/mod.rs`、flag `MNOTE_VAULT` 默认 0、API 测试
- **Dependencies**PR-S、PR1
- **Description**:§3.2 P0 路由;§3.5 语义;list/get mask;审计。flag 默认关。
### PR3 — SSR `/vault` + `/files` redirect
- **Title**`feat(vault): SSR /vault workbench shell`
- **Files**`ssr/pages/vault.rs`、page route、styles、HTML 无 secret 测试
- **Dependencies**PR2
- **Description**PageLayout 工作台壳 + bootstrap L0。
### PR4 — Sidebar 入口 + vault-workbench-runtime
- **Title**`feat(vault): sidebar entry and vault workbench runtime`
- **Files**`layout.rs`(链接 + **script 标签**)、`browser/vault-workbench-runtime.js``mnote-ui-runtime.js` import、回收站 tab、deep-link、dirty discard
- **Dependencies**PR3
- **Description**:完整 P0 UI;手动刷新;`rg setInterval` 干净。
### PR5 — Soft-delete UI 打磨与并发/revision(瘦身)
- **Title**`feat(vault): recycle-bin UX polish and revision conflict handling`
- **Files**runtime + 少量 API(若 baseRevision
- **Dependencies**PR4
- **Description****不再**塞 path deny / RAG(已在 PR-S)。专注回收站体验与两 tab 冲突。
### PR6 — Browser smoke
- **Title**`test(vault): browser smoke for vault CRUD and path deny`
- **Files**`scripts/task-vault-crud-smoke.js`、TESTING_REFERENCE 条目
- **Dependencies**PR4、PR-Sdeny 断言)
- **Description**:§ Phase P0-E 全部断言。
### PR7 — 迁移向导
- **Title**`feat(vault): import wizard from informal 个人/密码 notes`
- **Files**import API、UI、双轨警告、fixture 测试
- **Dependencies****PR2 + PR4**(稳定 create + UI
- **Description**preview/apply;不静默覆盖。
### PR8 — AI `mnote.vault.*`P1
- **Title**`feat(vault): Pi Lab mnote.vault tools with resolve default deny`
- **Files**`page_ai_pi/runtime.rs`、skills、短交叉引用 7-18
- **Dependencies**PR2、PR-S
- **Description**tools + 脱敏 + 回归 local_file deny。
### PR9 — 附件 cookies**不**强制依赖 PR8
- **Title**`feat(vault): vault attachment upload/download for cookies/session`
- **Files**attachment API + UIvault_store attachments
- **Dependencies**PR2、PR4UI 可后置);**不**依赖 PR8
- **Description**:专用 GET;禁 files/openpath 无 `..`
### PR10 — 文档收口
- **Title**`docs(vault): ARCHITECTURE, AGENTS, design README; move 12-1 to done`
- **Files**ARCHITECTURE、AGENTS、design/README、迁 done
- **Dependencies**PR6 绿;P0 可宣布
- **Description**:系统空间定位写清,避免 bug 误派 04-tree。
---
## Revision Summary(设计稿内)
2026-07-19 review 修订要点:
1. 全表面 path denyopen/stat/write/Pi resolve/local_file+ 合入门 PR-S。
2. 刷新模型改为 command/手动;撤销 watcher 假说。
3. P0 软删除 + restore/purge HTTP/UI 对齐。
4. Secret PATCH 语义与掩码哨兵拒绝。
5. SecretField / passwordHint / fields L0 合同。
6. Flag 默认关;PR5 瘦身;审计单 sink。
7. 鉴权矩阵冻结;resolve 默认 denyreveal 非 OS 手势。
8. Owner = mnote-web 系统空间,非假 kernel。
9. 附件路径与专用 GET;5-34 为启发。
10. 双轨 `个人/密码/` 风险与 checklist/PR/K13K19 同步。
2026-07-19 re-review 修订:
11. **D3** `GET /api/local-folder/resource/read``read_local_resource`)一等公民 denyD4 write / D5 upload 具名;表扩至 **D1D12**
12. 首选 choke`resolve_local_open_path`+ Pi resolve);`stat_local_file` 显式(不走 resolve)。
13. §9 / checklist 去掉幽灵 **P0-D**flag 门闩 = **P0-S**+ 建议 P0-E)。
14. `passwordHint == password`**仅** `400 vault_hint_equals_secret`K18)。
15. K20smoke 含 resource/read + stat。
16. §5.2 补 **live `/api/local-folder/*` 路由盘点**mod.rs):content 面仅 open/stat/resource/read|write/assets/uploadOCR 已退役;events/sync/cache 非正文旁路。