Files
mnote/design/12-vault/process/12-3-chrome-extension-vault-save-v1.md
T
Agent Board 262e66b02e feat: purge legacy agent hosts and land vault Chrome extension path
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.
2026-07-25 14:25:37 +08:00

930 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 12-3 [process] Chrome 扩展:站点登录/注册页保存到密码箱 v1
> 创建时间:2026-07-24
> 状态:`PROCESS`
> Owner`12-vault`(扩展客户端 + mnote-web 鉴权/跨域适配;**不**改 tree kernel / Page Aggregate
> 建议 repo 落点:`design/12-vault/process/12-3-chrome-extension-vault-save-v1.md`
>
> 上位依据:
> - `design/12-vault/process/12-1-password-vault-dedicated-crud-workbench-v1.md`vault 真源、CRUD API、L0/L1、同站多账号)
> - `design/12-vault/process/12-2-vaultd-local-token-agent-read-path-v1.md`agent 读密通道;**本设计不复用 agent token 做写入口**
> - `/home/lix/.agent-infra/vault-policy.md` + `$mnote-vault`AI 读密策略;扩展默认写**人用 vault**)
> - `ARCHITECTURE.md` local-first`AGENTS.md` 密码箱薄指针
---
## 1. Overview
### 1.1 一句话
> **Chrome MV3 扩展在用户完成 MNote 远程登录后,于外站注册/登录页弹出「保存到密码箱」;默认同时保存账号密码与浏览器登录态(Cookie);登录态以「每账号独立文件」落盘;Agent 经 `$mnote-vault` 的 `login` / `session` / `resolve --field session` 复用,与 `/vault` 同一真源。**
### 1.2 痛点
| 现状 | 问题 |
|------|------|
| 录入只靠 `/vault` 手填 | 跨站复制账号密码,摩擦高、易漏改 |
| 12-1 Non-Goals 写明「不做 OS 级自动填表扩展」 | 产品一期聚焦工作台合理;**录入侧**仍缺浏览器入口 |
| 12-2 agent token 只服务读密 | **不能**把 agent capability token 交给扩展当写凭证 |
### 1.3 与 12-1 / 12-2 的关系
| 维度 | 12-1 | 12-2 | **12-3(本文)** |
|------|------|------|------------------|
| 真源 | `{workspace}/.mnote/vault/**` | 不变 | **不变** |
| 人用写入口 | `/vault` + `/api/vault/*` | 旁路 | **+ 浏览器扩展**(同 API 语义) |
| Agent 读密 / 登录态 | HTTP(演进中) | vaultd + capability token`login`/`session` 已有 | **不经扩展 resolve**;扩展只 **写入** session 文件;Agent 仍走 12-2 CLI |
| 登录态存储 | frontmatter 内嵌 `loginSession`(现状) | 同左(AI 本) | **演进为每账号独立文件**(§5.6);兼容读 frontmatter |
| 鉴权主体 | 浏览器 session cookie | agent token | **人会话 / 扩展专用 human token** |
| 自动填表 | 非目标 | — | **P0 不做**P2 可选 |
### 1.4 硬门闩(产品)
扩展在以下任一不满足时,**禁止**弹出保存、禁止自动提交 vault 写请求:
1. 已配置 **远程 MNote 基址**`baseUrl`,如 `http://127.0.0.1:3000`
2. 已用 **MNote 账号/密码** 完成登录,且会话未过期
3. 已选定可写 **workspace `rootUri`**local_folder 的 `file://…`,与工作台一致)
未登录态 UI 只允许:Options 配置 +「连接密码箱」;工具栏徽章显示「未连接」。
---
## 2. Goals / Non-Goals
### 2.1 Goals
1. **MVP 保存闭环**:外站检测到 password 表单 → 用户确认 → 写入当前用户 vault(create 或更新同站条目)。
2. **默认保存登录态**:确认保存时 **默认勾选**「同时保存登录态」;扩展用 `chrome.cookies` 抓取目标 origin 的 Cookie,写入 **每账号独立 session 文件**(§5.6)。
3. **Options 引导**:远程地址 + 账号密码登录 + 选择 workspacerootUri+ 连接状态展示。
4. **与 `/vault` 同真源**`POST /api/vault/items``PATCH /api/vault/items/{id}``PUT …/session``GET /api/vault/list`;字段对齐 `VaultCreateInput` + session file schema。
5. **安全默认**:密钥与 Cookie 只经 extension **background** 出站;content script **不**持长期 token / cookie 明文;审计无 value / cookieHeader。
6. **可选同步 AI 本**:保存后二次确认才 `share-to-ai`;勾选时 **连同 session 文件** 复制到 AI 本(否则 Agent 无法 `login` 复用)。
7. **Skill / Agent 可调用**`$mnote-vault` + vault-policy 明确 `login` / `session` / `resolve --field session` 稳态(§6.5)。
8. **可安装、可调试**Manifest V3;本地 unpacked 加载;文档化 host_permissions(含 cookies)。
### 2.2 Non-Goals(本设计不承诺 / 明确不做)
| 项 | 说明 |
|----|------|
| P0 自动填充 | 不做 Bitwarden 式 autofill;仅「保存」 |
| Agent capability token 复用 | 扩展 **禁止** 使用 `mnote-vault` agent token / vaultd UDS 写库 |
| 改 kernel / tree.* | vault 仍非图节点 |
| 云端跨机 vault 同步 | 仍 local-first;远程只是 mnote-web 可达地址 |
| 完整密码管理器对标 | 无 TOTP 引擎、无浏览器原生 Password API 劫持(P2 再评估) |
| Firefox/Safari | P0 只 Chrome/Chromium MV3API 预留可移植 |
| 在 content script 内直接 fetch vault API | 禁止(token 面扩大) |
| 默认把所有保存项 share-to-ai | 禁止静默 |
### 2.3 成功标准(设计批准后实现门闩)
- [x] Options:填 baseUrl + 登录成功 → whoami 显示用户(扩展 CONNECT + API smoke 签发 mnext1
- [x] 未登录时打开任意站 password 表单 → **无**可保存(弹层提示去连接且禁用保存)
- [ ] 登录后在示例站提交登录表单 → 弹层预填 url/username/password → 确认 → `/vault` 列表可见新条目(**Chrome 手工**
- [x] **默认勾选「同时保存登录态」** → API smoke 确认磁盘 `sessions/{credId}/{accountId}.json`
- [x] 同 origin 已有条目 → 可选「追加账号」写入 `accounts[]`**新账号自有 session 文件**(扩展 UI
- [ ] share-to-ai 勾选时 AI 本同时有 session 文件;Agent `mnote-vault login --id``mode=session` 复用(**Chrome 手工**
- [x] `$mnote-vault` skill 写明登录态调用(`login` / `session`);vault-policy 待同步
- [x] audit 有 `session_put`L0 **无** password / cookieHeader 明文(smoke 断言)
- [x] 错误 baseUrl / 401 → 明确文案;token 吊销后 401smoke
---
## 3. 问题与方案选型
### 3.1 写入通道
| 方案 | 优点 | 缺点 | 结论 |
|------|------|------|------|
| A. 扩展 → mnote-web `/api/vault/*`(人会话) | 与工作台一致;已有 create/update | 需跨域鉴权 | **采用** |
| B. 扩展 → Native Messaging → 本机写文件 | 不依赖 3000 | 无「远程地址」;安装重;与 local-first 桌面强绑定 | P2 备选「纯本机」 |
| C. 扩展 → vaultd agent token | 不启 web | token 设计给 agent 读;无完整 human CRUD 策略 | **禁止** |
### 3.2 鉴权形态
| 方案 | 说明 | 推荐阶段 |
|------|------|----------|
| **E1. Session cookie 跨域** | `POST /api/auth` 后带 `mnote_session`;扩展 fetch `credentials: 'include'` | 本机 localhost 可试;跨站 SameSite 脆弱 |
| **E2. Extension human token(推荐)** | 登录成功后服务端签发 **仅 vault.view + vault.edit** 的 Bearer;扩展存 `chrome.storage.session` | **MVP 主路径** |
| **E3. 复用 `/api/auth/mnote-web-token`** | 若现网已有可给 API 的 token | 实现时审计 scope;不足则走 E2 |
**推荐默认(钉死):E2。**
Cookie 路径(E1)可作为同机 dev 旁路,**不得**作为唯一生产假设。
E2 token 逻辑声明(与 12-2 agent token **分 aud/scope**,禁止混用):
```json
{
"v": 1,
"iss": "mnote-web",
"aud": "chrome-extension-vault",
"sub": "user:<userId>",
"scope": ["vault.view", "vault.edit"],
"rootUriAllow": ["file:///…可选绑定…"],
"iat": 0,
"exp": 0,
"jti": "uuid"
}
```
- **禁止** scope 含 `vault.resolve` / AI resolve。
- **禁止** 与 `mnv1.*` agent token 同密钥、同 aud。
- TTL 默认 **7d**(可 Options 续期 / 重新登录);revoke 按 jti。
- 传输:`Authorization: Bearer <token>`
### 3.3 远程地址与 rootUri
```text
Options 配置:
baseUrl = https://mnote.example 或 http://127.0.0.1:3000
account = 用户账号/邮箱
password = 仅登录瞬间使用;成功后**不**长期明文落盘(见 §7)
rootUri = (可选)file://… 覆盖;默认自动解析
sourceKind = local_folder(固定)
```
- `baseUrl` 规范化:去尾 `/`;仅 `http:`/`https:`;禁止 `javascript:` 等。
- **首次连接**后把 baseUrl 写入 `chrome.storage.sync`(可同步)或 `local`token 只进 **session** storage。
- **默认 rootUri(钉死)**:登录成功后 `POST /api/local-folder/workspaces/default` → 账号固定
`…/users/<actor>/workspaces/my-space``file://` rootUri(与 web 壳默认 local_folder 一致)。
Options **不必手填**;「高级」仅用于写到其它已授权 local_folder。
- 多 workspace / 自定义目录:仍可覆盖 rootUri(P1 可做下拉列表)。
---
## 4. 目标架构
### 4.1 组件图
```text
┌─────────────────────────────────────────────────────────────┐
│ 外站页面(任意 origin) │
│ content script:侦测 password 字段 / 提交;发消息给 background │
│ 不持 token;不直接打 MNote API │
└────────────────────────────┬────────────────────────────────┘
│ chrome.runtime.sendMessage
┌─────────────────────────────────────────────────────────────┐
│ Extension Service Workerbackground
│ - 持 baseUrl + human token + rootUri │
│ - 保存弹层协调 / 工具栏 badge │
│ - fetch(`${baseUrl}/api/vault/...`) │
│ - host_permissions: 用户配置的 baseUrloptional 动态) │
└────────────────────────────┬────────────────────────────────┘
│ HTTPS + Bearer
┌─────────────────────────────────────────────────────────────┐
│ mnote-web │
│ POST /api/auth → 登录 │
│ POST /api/vault/extension/token → 签发 E2(新) │
│ GET /api/auth/whoami → 校验会话/token │
│ GET /api/vault/list → 同站匹配 │
│ POST /api/vault/items → create │
│ PATCH /api/vault/items/{id} → update / 追加账号 │
│ PUT /api/vault/items/{id}/session → 写每账号 session 文件 │
│ POST /api/vault/items/{id}/share-to-ai → 可选(含 session)│
└────────────────────────────┬────────────────────────────────┘
│ 文件写
{workspace}/.mnote/vault/
entries/{credId}.md # 账号密码真源
sessions/{credId}/{acc}.json # 每账号登录态(§5.6
```
### 4.2 仓库落点(建议)
| 路径 | 职责 |
|------|------|
| `extensions/mnote-vault/`(新建) | MV3 扩展源码:manifest、background、content、options、popup |
| `rust/crates/mnote-web/src/routes/vault_extension.rs`(或并入 `vault.rs` | extension token 签发/吊销;可选 match-by-url 查询 |
| `browser/vault-workbench-runtime.js` | 可选:「复制连接信息到扩展」 |
| `design/12-vault/process/12-3-…` | 本文 |
| 文档 | `scripts/TESTING_REFERENCE.md` 增补 extension smoke |
**不**放入 `recycle/`**不**改 `core-domain` 图模型。
### 4.3 Manifest V3 最小权限
```json
{
"manifest_version": 3,
"name": "MNote Vault",
"permissions": ["storage", "activeTab", "scripting", "alarms", "cookies"],
"optional_host_permissions": ["http://*/*", "https://*/*"],
"host_permissions": [],
"background": { "service_worker": "background.js", "type": "module" },
"action": { "default_popup": "popup.html" },
"options_page": "options.html",
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"run_at": "document_idle",
"all_frames": false
}]
}
```
说明:
- **`cookies` 权限**:默认保存登录态时,background 用 `chrome.cookies.getAll({ url })` 抓目标 origin Cookie**禁止** content script 读 cookie)。
- **optional_host_permissions**:用户在 Options 确认 baseUrl 后 `chrome.permissions.request` 只放行该 origin;目标站默认靠 content_scripts matches(仅读 DOM,不出站)。保存登录态时,对目标站 origin 也需 host 权限(`cookies` API 要求)。
- 若政策要求更紧:content_scripts 改为 `activeTab` + 用户点工具栏再注入(牺牲「自动侦测」)。**P0 推荐:全站 content script + 保存必须用户点确认**(非静默上传)。
---
## 5. 用户流程
### 5.1 首次连接(Options
```text
1. 打开扩展 Options
2. 输入 baseUrl →「测试连接」→ GET {baseUrl}/api/health 或 /api/auth/whoami(未登录可 401 但仍可达)
3. 输入 MNote 账号/密码 →「登录」
4. 扩展 background
POST {baseUrl}/api/auth { provider password, flow signIn, … }
成功后 POST {baseUrl}/api/vault/extension/token Cookie 或临时会话)
或 auth 响应内嵌 extensionToken(实现可选合并)
5. 选择/粘贴 rootUri →「保存配置」
6. Badge:已连接 · 用户 email 缩写
```
密码处理:
- Options 表单内存持有至登录成功;
- **成功后立即清除** DOM 与内存中的 password
- **禁止** `chrome.storage` 持久化 MNote 登录密码;
- 仅持久化:`baseUrl``rootUri``userId`/`email`(非敏感)、token 放 **session**(浏览器重启需重新登录或 refresh)。
### 5.2 外站保存(MVP
```text
触发(满足其一即可进入「候选」状态):
T1. 页面存在 input[type=password],且用户焦点离开该字段 / 输入长度≥阈值
T2. form submit 且 form 内含 password
T3. 用户点击扩展工具栏「保存当前页账号」
门闩:
未连接 → 仅 toast/小条:「打开 MNote Vault 扩展并登录」
已连接 → 弹出确认层(扩展 page 或 iframe 隔离 UI,见 §5.4
确认层字段:
- title(默认:document.title 截断 80 或 hostname
- url(默认:location.origin + pathname 的 login 路径;可编辑)
- username / email(从 autocomplete=username / email / name=user 启发式)
- password(来自 password input;可显示切换)
- 匹配结果:若 list 命中同站 → 「新建条目」|「追加到已有 · title」
- **「同时保存登录态」checkbox:默认勾选**(用户可取消)
- 可选 checkbox:同步到 AI 密码本(默认关;勾选时连同 session 文件)
- 按钮:保存 / 取消
确认后 background
1) create 或 update(账号密码)
2) 若「同时保存登录态」仍勾选:
chrome.cookies.getAll({ url: pageUrl 或 origin })
→ 拼 cookieHeader
→ PUT /api/vault/items/{id}/session
{ rootUri, accountId, cookieHeader, source: "chrome_extension" }
3) 若勾选 share-to-ai → POST share-to-ai(服务端复制 credential + session 文件)
4) toast 成功/失败(session 失败不回滚 credential,但提示「账号已存、登录态未写入」)
```
### 5.3 同站匹配规则(P0
服务端或客户端匹配(P0 可客户端 list 后过滤,条目量小时足够):
1. 取候选 URL 的 **registrable origin**`https://github.com`)与 path 前缀(可选)。
2. 对 list 中每条 credential 的 `url` + `urls[]`
- origin 相等 → 候选;
- 多条 → 按 `updatedAt` 降序展示前 N=5。
3. 用户选「追加到已有」:
- `PATCH` 合并 `accounts[]` 新 slot(生成 `acc_*` id);
- 顶层 `username`/`password` 是否更新:
- **默认**:仅追加 slot**不**改 primary 镜像(避免踩掉主账号);
- 勾选「设为主账号」时再写 primary。
实现可新增只读辅助(P1):
```text
GET /api/vault/match?rootUri=&url=https://example.com/login
→ { items: [ L0 投影… ] }
```
P0 无此 API 时用 `GET /api/vault/list` + 客户端过滤。
### 5.4 确认 UI 隔离
| 方式 | 说明 | 推荐 |
|------|------|------|
| Content script 注入 shadow DOM | 实现快;样式污染风险;密码进页面 JS 上下文 | MVP 可接受若确认层只读自 form |
| `chrome.action` popup 预填 | 需用户点图标;最安全 | 与 T3 共用 |
| 独立 extension 页 `chrome.windows.create` | 重 | 备选 |
**P0shadow DOM 确认层 + 工具栏手动入口双轨。**
密码值从 form 读入后立即通过 message 传 background,确认层可用 `type=password` 掩码展示;取消则丢弃。
### 5.5 注册页 vs 登录页
不强制区分 DOM:同一保存路径。
启发式:URL/path 含 `signup|register|sign-up` 时 title 默认加「注册」前缀;不改变 schema。
### 5.6 每账号独立登录态文件(核心演进)
#### 5.6.1 为何不继续只靠 frontmatter
| 现状(12-1 / 12-2 已落地) | 问题 |
|---------------------------|------|
| `loginSession` 嵌在 `entries/{credId}.md` frontmatter | **整条 credential 只有一份** session;同站多账号互相覆盖 |
| Cookie 与密码同文件 | 改 session 会 bump credential revision;审计/diff 面大 |
| 扩展默认抓 cookie 后无处按账号落盘 | 无法满足「每个账号各自登录态」 |
**钉死:登录态 = 独立文件,按 credential + account 一文件。**
frontmatter 内 `loginSession` **仅兼容读**(迁移期);新写入一律走 session 文件。
#### 5.6.2 磁盘布局
```text
{workspace}/.mnote/vault/
entries/
{credId}.md # 账号/密码/accounts[] 真源(无 cookie 明文)
sessions/
{credId}/
primary.json # 无 accounts[] 或主账号(accountId 省略 / "primary"
{accountId}.json # 对应 accounts[].id,如 acc_01H…
attachments/… # 既有附件(大 cookie 导出仍可走附件,非默认)
audit.jsonl
index.json
```
- 路径一律相对 vault root;禁止 `..`
- 文件权限:与 vault 一致(建议 `0600` 文件 / `0700` 目录)。
- **删除 credential** → 同步删除 `sessions/{credId}/` 整目录。
- **删除 account slot** → 删除对应 `{accountId}.json``primary` 若被新主账号替换则按 merge 规则重命名/覆盖。
- **share-to-ai** → 复制 `entries` 到 AI 本时 **一并复制** `sessions/{credId}/**` 到 AI vault 同相对路径;否则 Agent 无法 `login` 复用。
#### 5.6.3 Session 文件 schema
```json
{
"schema": "mnote.vault.session.v1",
"credentialId": "cred_01H…",
"accountId": "acc_01H…",
"cookieHeader": "session=…; other=…",
"cookies": [
{
"name": "session",
"value": "…",
"domain": ".example.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "lax",
"expirationDate": 1735689600
}
],
"origin": "https://example.com",
"expiresAt": "2026-07-31T00:00:00Z",
"lastLoginAt": "2026-07-24T12:00:00Z",
"source": "chrome_extension",
"updatedAt": "2026-07-24T12:00:00Z",
"revision": 1
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `schema` | 是 | 固定 `mnote.vault.session.v1` |
| `credentialId` / `accountId` | 是 | 与路径一致;`accountId` 无多账号时用 `"primary"` |
| `cookieHeader` | 推荐 | 拼好的 `Cookie` 请求头(Agent `login` 主消费) |
| `cookies[]` | 推荐(扩展写) | 结构化;便于工作台展示/过期判断;与 cookieHeader 同步 |
| `origin` | 推荐 | 抓取时的 origin |
| `expiresAt` | 推荐 | 会话逻辑过期(默认 now+7d,`MNOTE_VAULT_SESSION_TTL_HOURS` |
| `source` | 是 | `chrome_extension` \| `api` \| `human_bridge` \| `browser` \| `login_api` |
| `revision` | 是 | 乐观并发 |
**L0 投影**list/get 只暴露 `hasLoginSession` / `sessionExpiresAt` / 可选 `sessionAccountIds[]`**永不**返回 `cookieHeader` / `cookies[].value`
#### 5.6.4 Core 读路径(兼容)
`mnote-vault-core` / `login` / `put_ai_vault_session` 演进顺序:
1. **写**:始终写 `sessions/{credId}/{accountId}.json`;可选同步清掉 frontmatter `loginSession`(或保留镜像一期,二期去掉)。
2. **读(login 复用)**
a. 若指定 `--account-id` → 只读该文件;
b. 否则读 `primary.json`,否则 `accounts[0].id.json`
c. **fallback**frontmatter `loginSession`(旧数据)。
3. **新鲜度**:同现网 `login_session_is_fresh``expiresAt` + TTL)。
#### 5.6.5 扩展抓 Cookie 规则
```text
url = 确认层 url 或 tab.url 的 origin(至少 scheme+host+port
chrome.cookies.getAll({ url })
→ 过滤:
- 排除超大 value(单 cookie > 8KiB 截断并记 note
- 可选排除已知 tracking 名(P1 列表);P0 全量 origin cookies
→ cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join('; ')
→ cookies[] 保留 domain/path/secure/httpOnly/sameSite/expirationDate
```
- 仅 background 持有 cookie 明文;确认层 **不**展示完整 cookie 列表(可显示「已捕获 N 条 Cookie」)。
- 用户取消「同时保存登录态」→ 跳过 PUT session。
- 登录**前**表单提交瞬间可能尚无 session cookie
- P0:仍保存当时 cookies(可能为空或仅 CSRF);toast 可提示「若刚登录,建议登录成功后再点工具栏『更新登录态』」。
- P1`webNavigation` / 登录成功 URL 启发式后再二次捕获(另 checklist)。
#### 5.6.6 与 attachments 的关系
- 12-1 附件 `kind: cookies` **保留**为大文件/导出旁路。
- **默认路径是 session 文件**,不是附件。
- 禁止把 cookieHeader 再写进 notes_markdown。
---
## 6. API 合同
### 6.1 复用(已存在)
| 方法 | 路径 | 用途 | 鉴权 |
|------|------|------|------|
| POST | `/api/auth` | 登录拿会话 | 公开 |
| GET | `/api/auth/whoami``/api/auth/session` | 校验身份 | session/token |
| GET | `/api/vault/list?rootUri=&sourceKind=local_folder` | L0 列表 / 匹配 | view |
| POST | `/api/vault/items` | 创建 | edit |
| PATCH | `/api/vault/items/{id}` | 更新 / 追加账号 | edit |
| POST | `/api/vault/items/{id}/share-to-ai` | 可选同步 AI 本(**须含 session 文件** | edit |
| POST | `/api/vault/ensure` | 确保目录 | 可写 |
| PUT | `/api/vault/ai/items/{id}/session` | AI 本 session 回写(既有;演进写文件) | AI + session scope |
**Create body 示例(扩展 → mnote-web):**
```json
{
"rootUri": "file:///home/user/Notes",
"sourceKind": "local_folder",
"title": "GitHub",
"url": "https://github.com/login",
"username": "alice",
"password": "plaintext-from-form",
"email": "alice@example.com",
"tags": ["from-extension"],
"folderPath": "imported/browser",
"notesMarkdown": "Saved via MNote Vault extension"
}
```
响应:与现网一致,L0 投影(password **masked**)。
**Update 追加账号(语义对齐 12-1 accounts[]):**
```json
{
"rootUri": "file:///…",
"accounts": [
{ "id": "acc_existing", "label": "work", "username": "…", "password": "…" },
{ "id": "acc_new", "label": "from-extension", "username": "…", "password": "…" }
]
}
```
注意:`accounts` 在现网为 **全量替换**`VaultUpdateInput.accounts: Option<Vec<…>>`)。扩展必须 **先 get 再 merge 再 patch**,禁止只发新 slot 导致清空旧账号。
### 6.2 新增(mnote-webMVP 建议)
#### `POST /api/vault/extension/token`
**前置**:已登录 sessionCookie)或刚完成 auth 同请求链。
Request
```json
{
"clientId": "chrome-extension",
"extensionId": "optional-chrome-runtime-id",
"ttlHours": 168
}
```
Response
```json
{
"ok": true,
"result": {
"token": "mnext1.…",
"expiresAt": "2026-07-31T00:00:00Z",
"scope": ["vault.view", "vault.edit"],
"userId": "…",
"email": "…"
}
}
```
#### `POST /api/vault/extension/token/revoke`
Body`{ "jti": "…" }` 或吊销当前 Bearer。
#### `PUT /api/vault/items/{id}/session`(人用 / 扩展 · 新增)
**鉴权**session cookie 或 E2 Bearer`vault.edit`)。
**写目标**:用户 vault(非 AI 本);路径 `sessions/{id}/{accountId}.json`
Request
```json
{
"rootUri": "file:///home/user/Notes",
"sourceKind": "local_folder",
"accountId": "primary",
"cookieHeader": "session=abc; other=1",
"cookies": [
{
"name": "session",
"value": "abc",
"domain": ".example.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "lax",
"expirationDate": 1735689600
}
],
"origin": "https://example.com",
"expiresAt": "2026-07-31T00:00:00Z",
"source": "chrome_extension",
"expectedRevision": null
}
```
规则:
- `cookieHeader``cookies[]` 至少一个非空;两者皆有时以 `cookies[]` 重算 header 为准(防不一致)。
- `accountId` 缺省 → `"primary"`
- 响应 L0`{ ok, result: { credentialId, accountId, hasLoginSession: true, sessionExpiresAt, revision } }`**无** cookie 明文。
- audit`action=session_put``source=chrome_extension`,无 value。
#### `GET /api/vault/items/{id}/session`(可选 P1
- 默认 **不** 返回 cookie(仅 meta)。
- 人用工作台「查看登录态元数据」用;扩展 P0 **不需要** GET。
#### share-to-ai 合同补强
`POST /api/vault/items/{id}/share-to-ai` 实现必须:
1. 复制 credential 到 AI 本(既有);
2. **复制** `sessions/{id}/**` → AI vault 同相对路径;
3. 若仅有 frontmatter `loginSession` 无文件 → 迁移写出 AI 侧 `primary.json` 再清/保留源侧策略按 core 统一。
否则 Agent 勾选同步后仍 `login` 不到 cookie。
#### 鉴权中间件扩展
`RequestContext` 识别:
1. 现有 `mnote_session` cookie
2. **新增** `Authorization: Bearer mnext1.…`extension human token
映射到同一 `actor_id`vault 路径 `require_authenticated` 通过即可。
**reveal** 是否允许 extension token
- **P0:允许**(扩展未来 autofill 需要);须审计 `source=extension`
- 若产品收紧:P0 仅 list/create/updatereveal 仍要 web session
**推荐 P0extension token 允许 list / create / update / ensure / **session put**reveal 默认关(scope 不含 vault.reveal);P2 autofill 再加 `vault.reveal`。**
E2 scope 逻辑声明可写 `["vault.view", "vault.edit"]``vault.edit` **包含** session 文件写入(不必单独 `vault.session`,避免 token 面碎片化)。
### 6.5 Agent / Skill 调用登录态(`$mnote-vault` 合同)
> **扩展只写入;Agent 只经 12-2 CLI / core 消费。** 禁止 Agent 为读 session 去扫 `.mnote/vault/sessions/**` 文件。
#### 6.5.1 稳态命令(AI 必须会)
```bash
# 0. 环境(一次)
export MNOTE_VAULT_TOKEN_FILE="${MNOTE_VAULT_TOKEN_FILE:-$HOME/.config/mnote/vault-tokens/default.token}"
# 可选:MNOTE_AI_VAULT_ACTOR / MNOTE_VAULT_WORKSPACE
# 1. 选型(未知 id
mnote-vault list
# L0 可见 hasLoginSession / sessionExpiresAt(无 cookie 明文)
# 2. 优先复用登录态(推荐主路径)
mnote-vault login --id <credId>
# 成功:mode=session | reused → 响应含 cookieHeader(仅 stdout 一次;勿贴聊天)
# 过期/无 session:走 api_first 或 human_required
# 2b. 多账号时指定账号(session 文件按 accountId
mnote-vault login --id <credId> --account-id <accId>
# 3. 人机验证后回写(chrome-bridge / 扩展已写人用库后 share-to-ai
mnote-vault session --id <credId> --cookie-header 'name=value; …' --source human_bridge
# 可选:--account-id <accId>
# 4. 只要 cookie 明文管道(实现演进;与 login 二选一)
# mnote-vault resolve --id <credId> --field session [--account-id <accId>] [--raw]
```
| 意图 | 命令 | 读哪里 |
|------|------|--------|
| 复用未过期 Cookie 访问外站 | `login` | `sessions/{id}/{account}.json` → fallback frontmatter |
| 浏览器验证后落盘 | `session` | 写同上路径 |
| 只要密码填表 | `resolve --field password` | credential md |
| 只要 Cookie 不做登录流程 | `resolve --field session`P1 落地)或 `login``cookieHeader` | session 文件 |
#### 6.5.2 Agent 行为规则(写入 skill
1. **需要已登录外站状态时**:先 `login --id`,看 `mode``session`/`reused`/`ok` 用返回的 `cookieHeader` 调目标 API / 注入浏览器。
2. **禁止** `Read`/`cat`/`grep` `.mnote/vault/sessions/**` 或 credential 内 cookie。
3. **禁止**`cookieHeader` / token 写入聊天、commit、issue。
4. `human_required` → 用 chrome-bridge 完成验证 → `session` 回写;**不要**为 session 去起 mnote-web。
5. 人用扩展写入的 session 在 **用户 vault**Agent 默认读 **AI 本** → 须用户 **share-to-ai**(或显式配置 actor 指向人用本,非默认)。
6. 多账号:list/get 看 `accounts[]` + `hasLoginSession``login --account-id` 与扩展写入的 `accountId` 对齐。
#### 6.5.3 文档落点(实现 Checklist 同步改)
| 文件 | 变更 |
|------|------|
| `/home/lix/.agent-infra/vault-policy.md` | Session 语义改为「每账号文件」;登录稳态表保留 |
| `skills/mnote-vault/SKILL.md`(全局 symlink) | 增加「登录态文件 + 调用示例」专节 |
| 12-2 | 交叉引用:session 存储演进见 12-3 §5.6CLI 语义不变 |
| 12-1 schema | 注明 `loginSession` frontmatter 兼容;新写走 `sessions/` |
#### CORS(仅 extension 需要时)
若 background 为 extension origin
-`/api/vault/*``/api/vault/extension/*``/api/auth`
- `Access-Control-Allow-Origin: chrome-extension://<id>` **或** 动态反射已登记 extensionId
- `Access-Control-Allow-Headers: Authorization, Content-Type`
- 不需要 `Allow-Credentials` 若纯 Bearer
**更干净:不靠 CORS,仅用 extension host_permissions 直连**Chrome 扩展跨域 fetch 不受页面 CORS 限制)。
**实现结论:MVP 以 host_permissions + Bearer 为主;服务端 CORS 仅在将来 Web 页面桥接时需要。**
### 6.3 错误码(扩展可分支)
| code | HTTP | 扩展动作 |
|------|------|----------|
| `vault_auth_required` | 401 | 清 token,引导 Options 登录 |
| `vault_extension_token_invalid` | 401 | 重新签发 |
| `vault_extension_token_expired` | 401 | 重新登录 |
| `vault_root_required` / `vault_root_invalid` | 400 | Options 检查 rootUri |
| `vault_permission_denied` | 403 | 提示 workspace 无写权限 |
| `vault_revision_conflict` | 409 | re-get 后重试 merge |
| `vault_mask_sentinel_rejected` | 400 | 勿提交掩码串 |
| `network_unreachable` | — | 检查 baseUrl / 本机 3000 |
### 6.4 审计
沿用 `{workspace}/.mnote/vault/audit.jsonl`
```json
{"ts":"…","action":"create","actorId":"user_x","credentialId":"cred_…","requestId":"…","ok":true,"source":"chrome_extension"}
{"ts":"…","action":"session_put","actorId":"user_x","credentialId":"cred_…","accountId":"primary","requestId":"…","ok":true,"source":"chrome_extension"}
```
`source` 字段:实现时在 append_vault_audit 增加可选 `source`(无则兼容缺省)。**禁止** value / cookieHeader。
---
## 7. 安全设计
### 7.1 威胁与缓解
| 威胁 | 缓解 |
|------|------|
| 钓鱼站诱导保存到假 baseUrl | Options 显示完整 baseUrl;首次连接确认;可选证书钉扎不做 P0 |
| 恶意页面读 extension 消息 | content 只发「候选字段」;token 永不下行 content;响应不回传 password / cookie |
| XSS 页面窃取已输入密码 | 与浏览器密码管理器同类风险;确认层不把 token 注入页面 |
| token 失窃 | session storage;短 TTLrevokescope 无 resolve |
| Cookie 被 content 窃取 | 仅 background `chrome.cookies`;不回传 content |
| 磁盘 session 文件泄露 | vault 目录权限;path deny 对 agent 通用工具;只经 CLI |
| 扩展被恶意更新 | 自用 unpacked / 签名发布策略另案;P0 本地加载 |
| 日志/崩溃转储 | background 不 console.log password / cookieHeader;错误上报脱敏 |
| 静默批量上传 | **必须用户确认**;无「自动保存所有表单」默认开 |
| AI 本扩大攻击面 | share-to-ai 默认关 + 二次确认;复制 session 须同确认 |
### 7.2 与 12-1 安全分级
| 级 | 扩展行为 |
|----|----------|
| L0 | list/match 元数据可缓存内存短时(TTL≤5min),**不**持久化到 disk 含 hint 以外 secretsession 仅 `hasLoginSession` |
| L1 | create/update 提交明文 passwordsession put 提交 cookie **仅** HTTPS 到 baseUrl;扩展本地不落盘 |
| L2 | **不提供** resolve;禁止 agent tokensession **写**允许、**读明文**不对 extension |
### 7.3 与 chrome-bridge / QA 关系
- chrome-bridge 用于 agent **人机验证** session 回写(12-2),**不是**本扩展。
- 本扩展是 **人** 的录入工具;QA 可用 Playwright 装 unpacked 扩展做 smoke,不与 Hermes QA 默认路径耦合。
---
## 8. 扩展模块地图
```text
extensions/mnote-vault/
manifest.json
background/
index.js # 消息路由、API 客户端、badge
auth.js # login / token / whoami
vault-api.js # list/create/update/session/share
cookies.js # chrome.cookies → cookieHeader + cookies[]
match.js # origin 匹配
content/
detect.js # password 表单侦测
save-prompt.js # shadow DOM 确认层(默认勾选保存登录态)
options/
options.html
options.js
popup/
popup.html # 状态 + 快捷「保存本页」+「更新登录态」+ Options
popup.js
shared/
messages.js # 消息类型常量
normalize-url.js
README.md # 安装:chrome://extensions 开发者模式
```
### 8.1 消息协议(内部)
```ts
// content → background
type Msg =
| { type: "vault/candidate"; payload: { url: string; title: string; username?: string; email?: string; password: string; pageUrl: string } }
| { type: "vault/ping" };
// background → content
type Reply =
| { type: "vault/status"; connected: boolean; email?: string }
| { type: "vault/save-result"; ok: boolean; itemId?: string; sessionSaved?: boolean; message?: string };
// popup/options → background
type Ctrl =
| { type: "vault/login"; baseUrl: string; account: string; password: string }
| { type: "vault/logout" }
| { type: "vault/set-root"; rootUri: string }
| { type: "vault/save-confirmed"; payload: SavePayload }
| { type: "vault/refresh-session"; payload: { itemId: string; accountId?: string; pageUrl: string } };
type SavePayload = {
title: string;
url: string;
username?: string;
email?: string;
password: string;
matchItemId?: string; // 追加到已有
setPrimary?: boolean;
saveSession: boolean; // 默认 true
shareToAi: boolean; // 默认 false
pageUrl: string;
};
```
### 8.2 刷新模型
- **禁止** content/background `setInterval` 轮询 vault list。
- 匹配:保存前 **单次** list/match;结果可 memory cache 至多 5 分钟或 rootUri 变更时失效。
- 连接状态:登录/登出/401 时更新 badge;可用 `chrome.alarms` 做 token 到期前提醒(非轮询 vault)。
---
## 9. 分期
| 阶段 | 范围 | 依赖 |
|------|------|------|
| **P0 MVP** | Options 连接;Bearer;保存确认层;create**默认保存登录态 → session 文件**list 匹配;toolbar 手动保存;skill/policy 更新 | mnote-web token + session PUTcore 写 `sessions/` |
| **P1** | get+merge `accounts[]` + 每账号 session`login --account-id``/vault/match`share-to-ai 复制 sessionworkbench「复制到扩展」;登录成功后二次抓 cookie | P0 |
| **P2** | 自动填充(`vault.reveal`);`resolve --field session`;提交成功智能检测;Firefox | 安全评审 |
| **P3** | Native Messaging 纯本机旁路;导入浏览器 CSVTOTP;去掉 frontmatter `loginSession` 镜像 | 另设计 |
### 9.1 与 12-1 P2「协助录入」关系
12-1 §6 P2「协助录入确认流」可与本扩展合并叙事:**浏览器侧录入 = 12-3;工作台内粘贴助手可后置或不做。**
---
## 10. 实现 Checklist(批准设计后执行)
### PR-A — 设计与开关
- [x] 本文合入 `design/12-vault/process/`
- [x] 12-1 / 12-2 交叉引用 session 文件演进(§5.6
- [ ] Feature flag(可选):`MNOTE_VAULT_EXTENSION=1` 控制 token 路由是否注册(P0 随 `MNOTE_VAULT=1`
### PR-B — mnote-web + coresession 文件)
- [x] `POST /api/vault/extension/token` + revoke
- [x] Bearer `mnext1` 解析接入 `RequestContext` / vault 鉴权
- [x] scopeview+edit(含 session put);**无** resolve
- [x] `PUT /api/vault/items/{id}/session` → 写 `sessions/{id}/{accountId}.json`
- [x] core`put_login_session` / `login` 读路径优先 session 文件,fallback frontmatter
- [x] share-to-ai **复制** session 目录
- [x] audit `source=chrome_extension``session_put` 无 cookie 明文
- [x] 单测:签发、revoke、session 文件读写(`vault_extension_token` + `session_file_put_resolve_and_share_copy`
- [x] API smoke`scripts/vault-extension-api-smoke.js`auth → mnext1 → create → PUT session → disk → revoke
- [ ] P1`GET /api/vault/match``login --account-id`
### PR-C — 扩展 MVP
- [x] `extensions/mnote-vault` MV3 脚手架(含 `cookies` 权限)
- [x] OptionsbaseUrl / 登录 / rootUri
- [x] background vault-api + cookies 抓取
- [x] content 侦测 + 确认层(**默认勾选保存登录态**)
- [x] popup:状态 + 保存本页 + 更新登录态
- [x] README 安装步骤
### PR-D — Skill / Policy
- [x] 更新 `/home/lix/.agent-infra/vault-policy.md`(每账号 session 文件 + 调用)
- [x] 更新 `skills/mnote-vault/SKILL.md`login/session 与文件语义)
- [x] 确认全局 symlink`.grok` / Codex)→ 仓库 `skills/mnote-vault`
### PR-E — 验收
- [x] API 层:`node scripts/vault-extension-api-smoke.js` 对 3000 通过
- [ ] 本机:unpacked 扩展 Chrome 手工加载(用户验收)
- [ ] 测试账号登录扩展 → 保存 example → `/vault` 可见条目 + `sessions/…json`
- [ ] share-to-ai 后 `mnote-vault login --id``mode=session`
- [x] 未登录 content 禁用保存(仅提示去 Options)
- [ ] `TESTING_REFERENCE.md` 段落
### PR-F — P1 增强(可另 PR
- [x] 同站 accounts 合并 + 每账号 session(扩展 MATCH_URL + append mode
- [ ] workbench 复制连接信息
- [ ] 登录成功后二次抓 cookie
- [ ] `resolve --field session`
---
## 11. 验收场景(手工 / smoke
| # | 场景 | 期望 |
|---|------|------|
| S1 | 无配置打开 example.com 登录表 | 无保存弹层或仅「去连接」 |
| S2 | 配置错误 baseUrl | 测试连接失败,不存 token |
| S3 | 正确登录 + rootUri | badge 已连接 |
| S4 | 填用户名密码 → 点保存 → 确认(默认保存登录态) | create 成功;`sessions/{id}/primary.json` 存在;password reveal 正确 |
| S5 | 取消「同时保存登录态」 | 仅 credential,无新 session 文件 |
| S6 | 同站再存另一账号选「追加」 | accounts 两个;**各有** session 文件(若均勾选) |
| S7 | token 过期后保存 | 401 → 引导重新登录,不写半截 |
| S8 | 勾选同步 AI | AI 本有 credential **+** session`mnote-vault login` 可复用 |
| S9 | 不勾选同步 AI | 仅人用库有 sessionAI `login` 无该 id 或无 cookie |
| S10 | 审计文件 | 有 create / session_put,无明文密码/cookie 行 |
| S11 | skill 文档 | Agent 按 skill 用 `login`/`session`,不扫 vault 文件 |
---
## 12. Alternatives Considered
| 方案 | 结论 |
|------|------|
| 只做 bookmarklet 调 API | 无可靠 backgroundtoken 易进页面;否 |
| 浏览器原生 Password Manager 导出再 import | 不解决「当下保存」;可作迁移旁路 |
| 全部走 Native Host 写磁盘 | 无远程 baseUrl;与用户需求不符;P2 可选 |
| 扩展直接读 `.mnote/vault` 文件 | 违反 vault path deny 与加密演进;禁止 |
| 复用 agent `mnote-vault` token | 权限与 actor 错误;禁止 |
| 继续只嵌 frontmatter `loginSession` | 同站多账号互踩;否;演进为每账号文件 |
| session 只做 attachments 附件 | 路径长、list 难;默认 session 文件,附件作导出旁路 |
---
## 13. 待决问题(实现前钉死;推荐已标)
| # | 问题 | 推荐默认 |
|---|------|----------|
| D1 | 鉴权 E1 cookie vs E2 Bearer | **E2 Bearer**dev 可附带 cookie |
| D2 | content_scripts 全站 vs activeTab | **全站侦测 + 用户确认保存** |
| D3 | extension token 是否 reveal | **P0 否**P2 autofill 再开 |
| D4 | rootUri 如何获得 | **P0 手动 / 工作台复制**P1 列 directory grant |
| D5 | 默认 folderPath | `imported/browser` |
| D6 | 是否默认 tags | `["from-extension"]` |
| D7 | 扩展目录 monorepo 还是独立 repo | **monorepo `extensions/mnote-vault`** |
| D8 | 发布渠道 | P0 仅 unpacked / 自签;商店上架另案 |
| D9 | 与 12-1「不做自动填表」文案 | 修订交叉引用,避免矛盾 |
| D10 | 密码字段启发式失败 | 工具栏手动选中 / 用户可编辑确认层 |
| D11 | 默认是否保存登录态 | **是(默认勾选)**;用户可取消 |
| D12 | session 存 frontmatter vs 独立文件 | **独立文件** `sessions/{cred}/{acc}.json`frontmatter 只兼容读 |
| D13 | 无多账号时 accountId | **`primary`** |
| D14 | 登录瞬间 cookie 可能不全 | P0 仍写 + 工具栏「更新登录态」;P1 登录成功二次抓 |
---
## 14. 修订记录
| 日期 | 变更 |
|------|------|
| 2026-07-24 | 初稿:Chrome MV3 保存扩展;E2 human token;复用 vault CRUD;与 12-1/12-2 边界钉死 |
| 2026-07-24 | 增补:默认保存登录态;每账号独立 session 文件;PUT session APIskill/agent 调用合同 §6.5 |
---
## 15. 批准后下一步(给实现者)
1. 产品确认 §13 D1D5、**D11–D14**(默认可直接开工)。
2.**PR-B**token + session 文件 + 鉴权)再 **PR-C**(扩展),避免扩展对接空 API。
3. 同步 **PR-D** skill/policy,保证 Agent 知道 `login`/`session` 读的是 session 文件。
4. 不做 autofill 直到 P0 保存闭环(账号 + 登录态)绿 + 安全过目。
5. 扩展是 **人用录入**Agent 读密/读登录态 **仍只走** `$mnote-vault` CLI**不是**扩展通道。