Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
57 KiB
12-1 [process] 密码箱(Password Vault)专用 CRUD 工作台整体设计 + Checklist v1
创建时间:2026-07-19
修订时间:2026-07-19(+ 密文簿 cipher-book、folderPath 树分组、同站多账号;review 修订:path deny 合入门、软删合同、secret PATCH、刷新模型、PR 重排;re-review:resource/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.mddesign/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.mddesign/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.* 到 bridge;P0 以 /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
当前状态与痛点
- 非正式密码笔记(用户工作区观察,非仓库事实):部分用户工作区存在
个人/密码/下大量自由 Markdown 目录条目(用户环境曾观察到约 172 条量级),正文常含明文密码/token。无固定 schema、无统一入口、无 reveal 分级,且 仍可被 FileTree / 本地搜索 / LightRAG 索引——即使正式 vault 上线,在导入归档前该目录仍是泄露面(见 §7.4)。 - 左栏存在可改造入口:
layout.rsquick-actions 中inventory_2链到/files。代码核实:/files当前无注册 route(死链),适合改造为「密码箱」固定入口。 - 垃圾箱模式可借鉴但 IA 不同:4-37 是 modal workbench;用户明确要求密码箱是 专门页面。可复用 trash 的数据层:系统目录 ensure、index、SSR workbench、rootUri 鉴权。
- 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)。
- 架构约束:local-first、禁止 UI 拼第二套真相、浏览器主链禁止新增
setInterval、RAG 必须排除 vault。
代码锚点(已核实)
| 锚点 | 路径 / 符号 | 含义 |
|---|---|---|
| 左栏 quick-actions | ssr/pages/layout.rs L145–151 |
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 |
| 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 |
| 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
- 专用页面
/vault:完整 CRUD(列表 + 详情/表单),非 leptos-tiptap。 - 左栏固定入口「密码箱」:改造死链
/files,按当前用户 + workspacerootUri解析。 - 固定 credential 模板:核心
title/url/username/password;可选 email/apikey/token/notes/tags;cookies 等走附件(P1+)。 - 存储:
{workspaceRoot}/.mnote/vault/;frontmatter +mnote.kind: credential。 - mnote-web 系统空间 API:
vault.ensure|list|get|create|update|delete|restore|purge|reveal(P0)与稳定 projection JSON;P1 增加 resolve / AI tools。 - 安全分级 L0/L1/L2;通用文件面 path deny 为 secret 写入的硬前置。
- RAG / local search / FileTree 默认不可见、不可索引 vault。
- 可选导入
个人/密码/;不静默覆盖;双轨期风险明示。 - AI:P0 仅 deny 通用读;P1
mnote.vault.*;P2 录入/自注册/cookies。 - 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 入口
左栏 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 页面布局
┌─────────────────────────────────────────────────────────────┐
│ 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 / purge(P0) |
| 复制 / 显示密码 | copy 或 reveal;默认掩码;不写 localStorage |
| 刷新 | 仅:(1) command 成功后 local patch / re-fetch;(2) 工具栏「刷新」按钮。禁止 setInterval。不依赖现有 local folder watcher 推 vault 变更 |
1.4 P0 UI 验收(最小)
?id=cred_…deep-link:SSR/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 目录布局
{workspaceRoot}/
.mnote/
vault/ # 系统 ensure
vault-index.json # L0 索引(含 status)
audit.jsonl # P0 唯一审计 sink(append-only)
entries/{credentialId}.md
attachments/{credentialId}/cookies-{stableId}.txt
trash/
entries/
attachments/
ensure(扩展 ensure_default_workspace_directories + vault.ensure):
.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,禁止 ..):
---
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}.json(12-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):
// 仅出现在 API 响应,永不作为磁盘存储类型
type SecretFieldState =
| { state: "absent" } // 磁盘无值或空
| { state: "masked" } // 有值但不返回明文
| { state: "revealed"; value: string; expiresAt?: string }; // 仅 reveal/resolve 短时
存储侧 Rust(mnote-web vault_store,P0):
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-relative;normalize 后不得含 ..
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
{
"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 原子更新 index(temp + rename)。
- index 永不存 password/apikey/token 明文。
- Rebuild(index 损坏或不一致时):
- 扫描
entries/*.md与trash/entries/*.md。 - L0-only allowlist 解析:只抽取 id/title/url/username/email/passwordHint/tags/has* 标志/attachments 计数/status;丢弃 password/apikey/token 键,不得进入 debug map。
- 解析错误日志只含
credentialId/ 相对路径 / error code,禁止 frontmatter 原文或 secret 子串。 - 运维:删除损坏 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或 commandvault.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 | P1–P2 |
vault.export |
— | export | P2 |
3.1a 密文簿 / 分组 / 同站多账号(P0 产品补充)
-
密文簿(cipher-book)
- 磁盘:
{workspaceRoot}/.mnote/vault/cipher-book.json(schemamnote.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,永不投影片段明文。
- 磁盘:
-
站内多账号 / 多密钥(推荐,取代「同站新账号」拆条)
- 一条 credential = 一个站点/服务(左栏一条)。
accounts[]:每组可折叠包含username/email/password/passwordHint(+ 可选label);UI 用 + 账号 新增组。secrets[]:附录 API Key / Token / other(kind+label+value);UI 用 + 密钥 新增。- 顶层
username/password/apikey/token仍保留为 primary 镜像(首账号密码、首个 apikey/token),供 AIresolve/login兼容。 reveal支持accountId(密码)与secretId(附录密钥)。- 不再提供详情「同站新账号」按钮(多账号应写在同一条目内)。
- 兼容:同 URL 仍允许多条 credential(历史数据);列表 meta 展示
N 账号/N 密钥。
-
分组折叠(folderPath)
- 逻辑字段
folderPath(如个人/银行/招商),不是 vault 下真实子目录。 - 左栏按路径多级折叠;折叠状态可存 sessionStorage。
- 条目文件仍平铺在
entries/{id}.md。 - 表单 UX(已落地):下拉选择已有
folderPath+ 文本直接输入新路径(无弹窗、无空组、无「在此新建」);组随条目出现。
- 逻辑字段
-
同站 URL 折叠(历史兼容)
- 同一
folderPath节点下,相同非空 url 且 ≥2 条 时,再包一层可折叠「站点组」。 - 新录入应优先合并到
accounts[]/secrets[],避免同站拆多条。
- 同一
-
共享到 AI 密码本
POST /api/vault/items/{id}/share-to-ai:把条目(含 secret 模板)复制/更新到目标 actor 的 managed 默认工作区 vault。- 默认目标 actor:
mnote-e2e(envMNOTE_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注册 idmnote-vault(hermes/reasonix 可发现)。
3.2 HTTP 映射(完整 P0 + 附件骨架)
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 |
| 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 body(camelCase):
| 客户端传入 | 服务端行为 |
|---|---|
| 字段 缺省(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-fetch;checklist 优先有 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」作为默认。- reveal:authenticated session + audit,非生物/OS 手势。
P1 capabilities:再在 grant / ai_policy 写入 vault.view|edit|resolve|export 并做 enforcement helper;不阻塞 P0。
5.2 通用路径 Deny — P0 合入门(全表面清单)
共享谓词(单一实现,禁止复制粘贴分叉):
/// 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 point(PR-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 |
在此 deny,leaf 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 |
deny(choke) |
| 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 全文 |
deny(choke);合入门必测 |
| D4 | POST /api/local-folder/resource/write |
write_local_resource → resolve_local_file_open_path |
deny(choke) |
| 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 → deny(vault 只走 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——均已进 D1–D5。其它 local-folder 路由不构成 secret 正文旁路。Pi / SSR / OnlyOffice / tree write 见 D6–D12。
单元测试(每个 D1–D5、D7–D9 至少一条;D3 为合入门):
- D1 open / D2 stat / D3 resource/read:
.mnote/vault/entries/x.md→ 403vault_path_denied;响应体无 fixture password、password:样例串;D3 无text明文。 - D4 resource/write / D5 assets/upload 目标 vault → 403。
- D7–D9:
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 全绿(含 D1–D5、D7–D9;尤其 D3 resource/read)。
5.3 威胁模型(摘要)
| 威胁 | 严重度 | 缓解 |
|---|---|---|
| HTTP open/stat 读 vault | 高 | D1–D2 + 测试合入门 |
| HTTP resource/read 回全文 | 高 | D3 + choke resolve_local_open_path |
| resource/write · assets/upload 写 vault | 高 | D4–D5 |
| Agent local_file 读 vault | 高 | D7–D9 |
| RAG 索引 vault | 高 | exclude + 单测 |
| list/get 回明文 | 高 | mask + reveal 分端 |
| 日志打印 secret | 高 | reveal/resolve/rebuild 错误路径禁 value |
双轨 个人/密码/ |
高 | §7.4 警告 + 可选 exclude |
| 磁盘物理读取 | 高(接受) | 非加密声明 |
| 掩码写回 | 中 | sentinel reject |
| XSS 读 DOM | 中 | 最短明文展示 |
6. AI 分阶段
P0
- 无
mnote.vault.*工具暴露。 - D7–D9(及 HTTP D1–D5,含 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/resolveAPI;Pimnote.vault.list|get|resolve(默认 allow,plan 模式 resolve deny);receipt 脱敏 value;scripts/mnote-vault-cli.js。 - transcript:agent 应使用
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 不能单独声称「工作区密码已卫生」。在源目录仍存在期间:
- Empty state / 导入页 固定警告:「
个人/密码/等目录仍可能被搜索与知识库索引;请导入后归档。」 - 推荐 apply 后可选移动源到
个人/密码/_imported/,并将_imported或整树加入 knowledge/local-search excludePatterns(实现可复用已有 exclude 设置)。 - 可选(Phase M+):内置 exclude 候选
个人/密码/**(默认 不 静默全局排除,避免误伤同名笔记;UI 一键启用)。 - 设计稿与验收 不 把「端到端密钥卫生」标为 P0 done,除非源已导入或 exclude。
8. Observability
P0 唯一审计 sink:{workspaceRoot}/.mnote/vault/audit.jsonl(append-only)。
示例行:
{"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。
可选 metrics:list 延迟、reveal 计数、deny 计数。
运维:index 损坏 → POST /api/vault/reindex 或 list 自动 rebuild;说明写入 support runbook 一节(本设计 §2.3)。
9. Rollout
| 项 | 策略 |
|---|---|
| Feature flag | MNOTE_VAULT:默认 0(关)。允许默认开的门闩:P0-S 全绿(含 D1–D5、D7–D9 单测,尤其 D3 resource/read)+ 建议 P0-E smoke 绿后再按环境打开。无 P0-D 阶段名 |
| 入口 | flag 关:隐藏 quick-action、API 404 |
| 密钥写入 | 禁止 在 flag 默认开且无 path deny(P0-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-effort;SSR 首屏禁止 secret;path canonicalize + vault prefix deny。
Open Questions
- 图标
lockvskey?建议 lock。 - 是否永久废弃「文件总览」产品名?当前
/files仅 redirect。 - 已冻结 P0:read grant 无 vault.view;若产品要协作者只读 vault → 改 §5.1 矩阵另开变更。
- 已冻结:
vault.resolve默认 deny。 - 导入候选路径是否扩展
密码/、Passwords/?默认仅个人/密码。 - credentialId:建议 ULID/UUID。
- 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 只存 L0;rebuild allowlist 解析 | 防 secret 进索引/日志 |
| K6 | P0 软删除 + 工作台回收站;不进全局 trash | 降泄露;与 restore/purge API 同阶段 |
| K7 | L0/L1/L2;reveal=会话+审计 | 最小暴露 |
| K8 | 通用文件面 server deny vault;AI 仅 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+edit;read/share⇒无 vault;resolve 默认 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 / K1–K20 / §5.1 矩阵 / §3.5 PATCH
- 冻结 deny 表面 D1–D12 与
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→ 403vault_path_denied;D3 响应无"text"明文 / fixture password - D4 resource/write / D5 assets/upload 目标 vault → 403
- D6 至少一条 page-body 或 tree 写路径 deny 单测
- D7–D9 local_file read/patch + resolve_file_path deny
- knowledge_rag:vault 路径不可 ingest(单测)
- local_search:vault 不进 index;用户 includePaths 强指 vault → 拒绝
- FileTree 不可见(
.mnote+ 断言) MNOTE_VAULT默认 0 时行为符合 §9(门闩引用 P0-S / P0-E,无 P0-D)
Phase P0-A — 存储
- ensure 含 vault + trash 子目录
- vault_store:entry 读写、index 原子更新、L0 allowlist rebuild
- soft-delete 移动文件 + status
- restore / purge
- audit.jsonl append(reveal/delete/…)
- parse error 日志无 password 单测
cargo test -p mnote-web:模块名建议vault_store/vault_path/vault_api(固定,不用「或等价」含糊)
Phase P0-B — HTTP
- 注册 §3.2 全部 P0 路由
- get/list mask;reveal 审计无 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.jsmodule script(同侧其它 browser runtime),并在mnote-ui-runtime.js侧 import 初始化 - deep-link
?id=、dirty discard、回收站 tab、手动刷新 - 无 setInterval:对
vault-workbench-runtime.js做静态扫描(rg setInterval) /files→/vault302
Phase P0-E — Smoke
scripts/task-vault-crud-smoke.js:- 登录 e2e 账号;开 root;ensure
- create → list 见 L0 → get 无明文 password
- update title only → 磁盘 password 不变
- reveal → 得明文;audit.jsonl 有行且
rg无 password 值 - delete → trash 路径存在;restore;purge 干净
GET .../files/open?path=.mnote/vault/entries/...→ 403GET .../resource/read?path=.mnote/vault/entries/...→ 403,body 无 fixture password / 无 secrettextGET .../files/stat?path=.mnote/vault/entries/...→ 403(或等价 deny,无敏感元数据滥用)- SSR
/vaultHTML 不含 fixture password 字符串
- 禁止 sqlite3 直写 CP
Phase M — 迁移
- preview/apply;冲突策略;双轨警告 UI
- 可选 archive + exclude 提示
- fixture 假密码导入 smoke(3 条)
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/export;ARCHITECTURE + 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.rstrash、page_ai_pi/runtime.rs、core-protocolAiAccessScope、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_pathchoke、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:实现 D1–D12 中的可触达面;必须含 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_sourceensure、vault_store.rs、audit.jsonl、单元测试vault_store - Dependencies:PR-S(或同 PR 内先 path deny)
- Description:目录、md 合同、index、soft-delete/restore/purge 文件层;无 HTTP 或仅测内函数。
PR2 — Vault HTTP API(CRUD + 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、flagMNOTE_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.jsimport、回收站 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-S(deny 断言)
- 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 + UI;vault_store attachments
- Dependencies:PR2、PR4(UI 可后置);不依赖 PR8
- Description:专用 GET;禁 files/open;path 无
..。
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 修订要点:
- 全表面 path deny(open/stat/write/Pi resolve/local_file)+ 合入门 PR-S。
- 刷新模型改为 command/手动;撤销 watcher 假说。
- P0 软删除 + restore/purge HTTP/UI 对齐。
- Secret PATCH 语义与掩码哨兵拒绝。
- SecretField / passwordHint / fields L0 合同。
- Flag 默认关;PR5 瘦身;审计单 sink。
- 鉴权矩阵冻结;resolve 默认 deny;reveal 非 OS 手势。
- Owner = mnote-web 系统空间,非假 kernel。
- 附件路径与专用 GET;5-34 为启发。
- 双轨
个人/密码/风险与 checklist/PR/K13–K19 同步。
2026-07-19 re-review 修订:
- D3
GET /api/local-folder/resource/read(read_local_resource)一等公民 deny;D4 write / D5 upload 具名;表扩至 D1–D12。 - 首选 choke:
resolve_local_open_path(+ Pi resolve);stat_local_file显式(不走 resolve)。 - §9 / checklist 去掉幽灵 P0-D;flag 门闩 = P0-S(+ 建议 P0-E)。
passwordHint == password→ 仅400 vault_hint_equals_secret(K18)。- K20;smoke 含 resource/read + stat。
- §5.2 补 live
/api/local-folder/*路由盘点(mod.rs):content 面仅 open/stat/resource/read|write/assets/upload;OCR 已退役;events/sync/cache 非正文旁路。