# 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 引导**:远程地址 + 账号密码登录 + 选择 workspace(rootUri)+ 连接状态展示。 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 MV3;API 预留可移植 | | 在 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 吊销后 401(smoke) --- ## 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:", "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 `。 ### 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//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 Worker(background) │ │ - 持 baseUrl + human token + rootUri │ │ - 保存弹层协调 / 工具栏 badge │ │ - fetch(`${baseUrl}/api/vault/...`) │ │ - host_permissions: 用户配置的 baseUrl(optional 动态) │ └────────────────────────────┬────────────────────────────────┘ │ 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` | 重 | 备选 | **P0:shadow 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>`)。扩展必须 **先 get 再 merge 再 patch**,禁止只发新 slot 导致清空旧账号。 ### 6.2 新增(mnote-web,MVP 建议) #### `POST /api/vault/extension/token` **前置**:已登录 session(Cookie)或刚完成 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/update,reveal 仍要 web session **推荐 P0:extension 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 # 成功:mode=session | reused → 响应含 cookieHeader(仅 stdout 一次;勿贴聊天) # 过期/无 session:走 api_first 或 human_required # 2b. 多账号时指定账号(session 文件按 accountId) mnote-vault login --id --account-id # 3. 人机验证后回写(chrome-bridge / 扩展已写人用库后 share-to-ai) mnote-vault session --id --cookie-header 'name=value; …' --source human_bridge # 可选:--account-id # 4. 只要 cookie 明文管道(实现演进;与 login 二选一) # mnote-vault resolve --id --field session [--account-id ] [--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.6;CLI 语义不变 | | 12-1 schema | 注明 `loginSession` frontmatter 兼容;新写走 `sessions/` | #### CORS(仅 extension 需要时) 若 background 为 extension origin: - 对 `/api/vault/*` 与 `/api/vault/extension/*`、`/api/auth`: - `Access-Control-Allow-Origin: chrome-extension://` **或** 动态反射已登记 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;短 TTL;revoke;scope 无 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 以外 secret;session 仅 `hasLoginSession` | | L1 | create/update 提交明文 password;session put 提交 cookie **仅** HTTPS 到 baseUrl;扩展本地不落盘 | | L2 | **不提供** resolve;禁止 agent token;session **写**允许、**读明文**不对 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 PUT;core 写 `sessions/` | | **P1** | get+merge `accounts[]` + 每账号 session;`login --account-id`;`/vault/match`;share-to-ai 复制 session;workbench「复制到扩展」;登录成功后二次抓 cookie | P0 | | **P2** | 自动填充(`vault.reveal`);`resolve --field session`;提交成功智能检测;Firefox | 安全评审 | | **P3** | Native Messaging 纯本机旁路;导入浏览器 CSV;TOTP;去掉 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 + core(session 文件) - [x] `POST /api/vault/extension/token` + revoke - [x] Bearer `mnext1` 解析接入 `RequestContext` / vault 鉴权 - [x] scope:view+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] Options:baseUrl / 登录 / 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 | 仅人用库有 session;AI `login` 无该 id 或无 cookie | | S10 | 审计文件 | 有 create / session_put,无明文密码/cookie 行 | | S11 | skill 文档 | Agent 按 skill 用 `login`/`session`,不扫 vault 文件 | --- ## 12. Alternatives Considered | 方案 | 结论 | |------|------| | 只做 bookmarklet 调 API | 无可靠 background;token 易进页面;否 | | 浏览器原生 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 API;skill/agent 调用合同 §6.5 | --- ## 15. 批准后下一步(给实现者) 1. 产品确认 §13 D1–D5、**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,**不是**扩展通道。