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.
This commit is contained in:
Agent Board
2026-07-25 14:25:37 +08:00
parent bc6f8488ee
commit 262e66b02e
137 changed files with 9018 additions and 46049 deletions
@@ -0,0 +1,929 @@
# 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**不是**扩展通道。