2026-07-21 05:13:05 +08:00
|
|
|
|
# 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.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.*` 到 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
|
|
|
|
|
|
|
|
|
|
|
|
### 当前状态与痛点
|
|
|
|
|
|
|
|
|
|
|
|
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` 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|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/tags;cookies 等走附件(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 JSON;P1 增加 resolve / AI tools。
|
|
|
|
|
|
6. 安全分级 L0/L1/L2;**通用文件面 path deny 为 secret 写入的硬前置**。
|
|
|
|
|
|
7. RAG / local search / FileTree 默认不可见、不可索引 vault。
|
|
|
|
|
|
8. 可选导入 `个人/密码/`;不静默覆盖;双轨期风险明示。
|
|
|
|
|
|
9. AI:P0 仅 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。
|
2026-07-25 14:25:37 +08:00
|
|
|
|
- 不做 OS 级 / 浏览器原生级 **自动填表** 密码管理器(autofill 引擎非本设计目标)。
|
|
|
|
|
|
- **Chrome「保存到密码箱」扩展**(登录/注册页确认后写入 vault)见独立稿:`design/12-vault/process/12-3-chrome-extension-vault-save-v1.md`(12-3);与本 Non-Goal 不冲突——12-3 P0 仅保存、不做 autofill。
|
2026-07-21 05:13:05 +08:00
|
|
|
|
- **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 / 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 目录布局
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
{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`):
|
|
|
|
|
|
|
|
|
|
|
|
```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: ""
|
2026-07-24 11:36:06 +08:00
|
|
|
|
# 站内多账号(折叠组);顶层 username/password 为 accounts[0] 镜像
|
|
|
|
|
|
accounts:
|
|
|
|
|
|
- id: acc_...
|
|
|
|
|
|
label: work
|
|
|
|
|
|
username: alice
|
|
|
|
|
|
email: alice@example.com
|
|
|
|
|
|
password: <example-template>
|
|
|
|
|
|
passwordHint: "Li@[A]***"
|
2026-07-25 14:25:37 +08:00
|
|
|
|
# 登录态(Cookie)不写在本 frontmatter 新路径:
|
|
|
|
|
|
# 见 sessions/{id}/{accountId}.json(12-3 §5.6);
|
|
|
|
|
|
# 旧字段 loginSession 仅兼容读。
|
2026-07-24 11:36:06 +08:00
|
|
|
|
# 附录密钥(API Key / Token);顶层 apikey/token 为首个同类镜像
|
|
|
|
|
|
secrets:
|
|
|
|
|
|
- id: sec_...
|
|
|
|
|
|
kind: apikey
|
|
|
|
|
|
label: personal-token
|
|
|
|
|
|
value: ""
|
2026-07-21 05:13:05 +08:00
|
|
|
|
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 短时
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**存储侧 Rust(mnote-web vault_store,P0)**:
|
|
|
|
|
|
|
|
|
|
|
|
```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-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`
|
|
|
|
|
|
|
|
|
|
|
|
```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 损坏或不一致时):
|
|
|
|
|
|
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 | P1–P2 |
|
|
|
|
|
|
| `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`,**永不**投影片段明文。
|
|
|
|
|
|
|
2026-07-24 11:36:06 +08:00
|
|
|
|
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 密钥`。
|
2026-07-21 05:13:05 +08:00
|
|
|
|
|
|
|
|
|
|
3. **分组折叠(folderPath)**
|
|
|
|
|
|
- 逻辑字段 `folderPath`(如 `个人/银行/招商`),**不是** vault 下真实子目录。
|
|
|
|
|
|
- 左栏按路径多级折叠;折叠状态可存 sessionStorage。
|
|
|
|
|
|
- 条目文件仍平铺在 `entries/{id}.md`。
|
|
|
|
|
|
- **表单 UX(已落地)**:下拉选择已有 `folderPath` + 文本直接输入新路径(无弹窗、无空组、无「在此新建」);组随条目出现。
|
|
|
|
|
|
|
2026-07-24 11:36:06 +08:00
|
|
|
|
4. **同站 URL 折叠(历史兼容)**
|
2026-07-21 05:13:05 +08:00
|
|
|
|
- 同一 `folderPath` 节点下,**相同非空 url 且 ≥2 条** 时,再包一层可折叠「站点组」。
|
2026-07-24 11:36:06 +08:00
|
|
|
|
- 新录入应优先合并到 `accounts[]`/`secrets[]`,避免同站拆多条。
|
2026-07-21 05:13:05 +08:00
|
|
|
|
|
|
|
|
|
|
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` 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 合入门(全表面清单)**
|
|
|
|
|
|
|
|
|
|
|
|
**共享谓词**(单一实现,禁止复制粘贴分叉):
|
|
|
|
|
|
|
|
|
|
|
|
```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 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` → 403 `vault_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` / `resolve` API;Pi `mnote.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。
|
2026-07-24 11:36:06 +08:00
|
|
|
|
- **演进(读密 transport,未实现)**:策略语义不变;agent 读密改走本地 **vaultd + capability token**、**不依赖 mnote-web**。见 `design/12-vault/process/12-2-vaultd-local-token-agent-read-path-v1.md`。
|
2026-07-21 05:13:05 +08:00
|
|
|
|
|
|
|
|
|
|
#### P2
|
|
|
|
|
|
|
|
|
|
|
|
- 协助录入确认流、attachment cookies、自注册回写、export;按 credential/run grant(可选)。
|
2026-07-25 14:25:37 +08:00
|
|
|
|
- 浏览器侧录入主路径收敛到 **12-3 Chrome 扩展**;工作台内粘贴助手可后置或不做。
|
2026-07-21 05:13:05 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### 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。
|
|
|
|
|
|
|
|
|
|
|
|
可选 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
|
|
|
|
|
|
|
|
|
|
|
|
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 只存 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`** → 403 `vault_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.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 账号;开 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/...` → 403
|
|
|
|
|
|
- `GET .../resource/read?path=.mnote/vault/entries/...` → 403,body 无 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 假密码导入 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.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**:实现 **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_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 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`、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-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 修订要点:
|
|
|
|
|
|
|
|
|
|
|
|
1. 全表面 path deny(open/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 默认 deny;reveal 非 OS 手势。
|
|
|
|
|
|
8. Owner = mnote-web 系统空间,非假 kernel。
|
|
|
|
|
|
9. 附件路径与专用 GET;5-34 为启发。
|
|
|
|
|
|
10. 双轨 `个人/密码/` 风险与 checklist/PR/K13–K19 同步。
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 re-review 修订:
|
|
|
|
|
|
|
|
|
|
|
|
11. **D3** `GET /api/local-folder/resource/read`(`read_local_resource`)一等公民 deny;D4 write / D5 upload 具名;表扩至 **D1–D12**。
|
|
|
|
|
|
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. K20;smoke 含 resource/read + stat。
|
|
|
|
|
|
16. §5.2 补 **live `/api/local-folder/*` 路由盘点**(mod.rs):content 面仅 open/stat/resource/read|write/assets/upload;OCR 已退役;events/sync/cache 非正文旁路。
|