Files
mnote/design/12-vault/process/12-1-password-vault-dedicated-crud-workbench-v1.md
T

1051 lines
57 KiB
Markdown
Raw Normal View History

# 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 级 / 浏览器原生级 **自动填表** 密码管理器(autofill 引擎非本设计目标)。
- **Chrome「保存到密码箱」扩展**(登录/注册页确认后写入 vault)见独立稿:`design/12-vault/process/12-3-chrome-extension-vault-save-v1.md`12-3);与本 Non-Goal 不冲突——12-3 P0 仅保存、不做 autofill。
- **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: ""
# 站内多账号(折叠组);顶层 username/password 为 accounts[0] 镜像
accounts:
- id: acc_...
label: work
username: alice
email: alice@example.com
password: <example-template>
passwordHint: "Li@[A]***"
# 登录态(Cookie)不写在本 frontmatter 新路径:
# 见 sessions/{id}/{accountId}.json12-3 §5.6);
# 旧字段 loginSession 仅兼容读。
# 附录密钥(API Key / Token);顶层 apikey/token 为首个同类镜像
secrets:
- id: sec_...
kind: apikey
label: personal-token
value: ""
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. **站内多账号 / 多密钥(推荐,取代「同站新账号」拆条)**
- 一条 credential = 一个站点/服务(左栏一条)。
- **`accounts[]`**:每组可折叠包含 `username` / `email` / `password` / `passwordHint`+ 可选 `label`);UI 用 **+ 账号** 新增组。
- **`secrets[]`**:附录 API Key / Token / other`kind` + `label` + `value`);UI 用 **+ 密钥** 新增。
- 顶层 `username`/`password`/`apikey`/`token` 仍保留为 **primary 镜像**(首账号密码、首个 apikey/token),供 AI `resolve`/`login` 兼容。
- `reveal` 支持 `accountId`(密码)与 `secretId`(附录密钥)。
- **不再提供**详情「同站新账号」按钮(多账号应写在同一条目内)。
- 兼容:同 URL 仍允许多条 credential(历史数据);列表 meta 展示 `N 账号` / `N 密钥`
3. **分组折叠(folderPath**
- 逻辑字段 `folderPath`(如 `个人/银行/招商`),**不是** vault 下真实子目录。
- 左栏按路径多级折叠;折叠状态可存 sessionStorage。
- 条目文件仍平铺在 `entries/{id}.md`
- **表单 UX(已落地)**:下拉选择已有 `folderPath` + 文本直接输入新路径(无弹窗、无空组、无「在此新建」);组随条目出现。
4. **同站 URL 折叠(历史兼容)**
- 同一 `folderPath` 节点下,**相同非空 url 且 ≥2 条** 时,再包一层可折叠「站点组」。
- 新录入应优先合并到 `accounts[]`/`secrets[]`,避免同站拆多条。
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。
- **演进(读密 transport,未实现)**:策略语义不变;agent 读密改走本地 **vaultd + capability token**、**不依赖 mnote-web**。见 `design/12-vault/process/12-2-vaultd-local-token-agent-read-path-v1.md`
#### P2
- 协助录入确认流、attachment cookies、自注册回写、export;按 credential/run grant(可选)。
- 浏览器侧录入主路径收敛到 **12-3 Chrome 扩展**;工作台内粘贴助手可后置或不做。
---
### 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 非正文旁路。