# dsh-vault — 加密凭据保险库插件

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![npm version](https://img.shields.io/npm/v/dsh-vault?color=cb3837&logo=npm)](https://www.npmjs.com/package/dsh-vault)
[![GitHub Release](https://img.shields.io/github/v/release/Ox0400/dsh-vault?logo=github)](https://github.com/Ox0400/dsh-vault/releases)
[![npm downloads](https://img.shields.io/npm/dm/dsh-vault)](https://www.npmjs.com/package/dsh-vault)
[![GitHub issues](https://img.shields.io/github/issues/Ox0400/dsh-vault)](https://github.com/Ox0400/dsh-vault/issues)
[![已收录: awesome-dsh-plugin](https://img.shields.io/badge/已收录-awesome--dsh--plugin-2ea44f)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/README.zh.md#L2869)
[![已收录: awesome-deepseek-harness](https://img.shields.io/badge/已收录-awesome--deepseek--harness-2ea44f)](https://github.com/Dominic789654/awesome-deepseek-harness/blob/main/README.zh-CN.md#L903)

[English](README.md) | **中文**

dsh-vault 是一个面向 DeepSeek Harness 的安全加密插件：把你在使用 AI 过程中产生的**用户名、邮箱、手机号、密码、二次动态密钥（TOTP）** 以及开发工作流中常用的 **SSH 连接、API Key、Secret、OAuth access/refresh token** 等敏感凭据加密存储，并通过模型工具提供增删改查、检索、密码生成与动态验证码生成能力。

**安全性与实现**

- **零外部依赖**：全部加密基于 Node 内置 `node:crypto`（AES-256-GCM 认证加密 + scrypt 密钥派生 + RFC 6238 TOTP），不引入任何第三方加密库。
- **主密码**：所有条目由 `scrypt(主密码, 盐)` 派生的 256 位密钥经 **AES-256-GCM** 加密。密钥永不落盘；进程内解锁后缓存复用（避免每次写入重复 scrypt），重启后重新派生。
- **防篡改**：GCM 认证标签 + 文档内固定明文的校验信封，错误主密码、密文被改动都会立即失败，绝不返回垃圾数据。
- **磁盘无明文**：加密文档中不含任何明文凭据；每个条目独立随机 nonce。
- **原子写入**：复用 harness 的 `writeFileAtomic` + 文件锁，任何时刻磁盘上都是完整的新/旧文档；进程内写入串行化、跨进程写入加锁。
- **检索不泄密**：`vault_search` 只返回摘要（id/标题/分类/用户名/邮箱/手机/主机/端口/URL/标签），**绝不返回密码、密钥、令牌与 TOTP 密钥**；完整凭据只能通过 `vault_get` 按 id 显式读取。

## 条目模型

每条记录包含一个 `title`、一个可选 `kind` 分类，以及任意组合的字段：

| 字段 | 说明 |
|---|---|
| `kind` | `login`（默认）/ `ssh` / `api-key` / `secret` / `oauth` / `cookie` / `card` / `custom` |
| `username` / `email` / `phone` | 账号身份 |
| `password` | 密码 |
| `host` / `port` | SSH 主机与端口（如 `db.internal` / `2222`） |
| `privateKey` | SSH 私钥（PEM） |
| `apiKey` | API 密钥 |
| `secret` | 通用 Secret（client secret、共享密钥等） |
| `accessToken` / `refreshToken` / `expiresAt` | OAuth 令牌对与过期时间（epoch millis） |
| `otpSecret` | TOTP 密钥（Base32 或 otpauth:// URI） |
| `url` / `notes` / `tags` | 元信息 |

其中两种 kind 有各自的形状:`cookie` 条目保存捕获的浏览器会话;`card` 条目保存 `cardNumber` / `cardExpiry`(MM/YY)/ `cardCvv` / `cardHolder` —— 检索摘要只暴露到期日与持卡人,绝不给卡号与 CVV;导出 Bitwarden JSON 时映射为 `card` 项(type 3,品牌自动推断)。
| `fields` | 任意附加键值（如 `{"region": "us-east-1"}`），可检索 |

## 工具

共 113 个工具,分五组。**基础**是默认安装就会注册的;其余通过[工具分级](#工具分级工具集)按需开启（设置 → 凭据库 → 权限 → 模型工具集）。

### 基础（默认注册） — 11

共 11 个工具。下面各组都需按需开启。

| 工具 | 说明 |
|---|---|
| `vault_list` | 列出保险库目录下可用的保险库（一个 .json 一个,排除 access/meta/导出等旁车文件） |
| `vault_search` | 跨标题/分类/用户名/邮箱/手机/主机/端口/URL/备注/标签/自定义字段（含数字/布尔/嵌套值；空格分隔多词 OR 命中）检索；可选 `createdAfter`/`createdBefore` 毫秒时间戳过滤；返回无密摘要；`limit` 须为 1–100 的整数 |
| `vault_get` | 按 id 读取完整条目（含全部密钥） |
| `vault_add` | 新增条目（上述字段任意组合；空字符串/空数组字段会被忽略） |
| `vault_update` | 按 id 更新字段（未提供的字段保留；空字符串清除该字段；`title` 可改名；`rotationDays: 0` 清除轮换 = 永不轮换） |
| `vault_delete` | 软删除条目（移入回收站，磁盘上仍加密保留） |
| `vault_fill` | 按 host/URL/用户名/标题匹配条目并返回其凭据 |
| `vault_clipboard` | 取出某条目某个字段的值用于直接复制（用户名/密码/apiKey 等） |
| `vault_totp` | 为存储的 otpSecret（或直接传入的 Base32/otpauth URI）生成当前 6 位动态验证码 |
| `vault_generate_password` | 生成强随机密码（长度/字符集/分组可选）**或易记口令短语**（`passphrase: true`，EFF 词表，`words`/`separator`/`wordDigits`） |
| `vault_strength` | 零依赖密码强度评估（0–100 分，weak/fair/strong/very strong） |

### 管理（标准档及以上） — 53

收藏、标签、活动历史、健康检查、模板、附件,以及卡与 2FA 的细节。

| 工具 | 说明 |
|---|---|
| `vault_compare` | 逐字段比较两个条目（`onlyA`/`onlyB`/`differ`/`equal`）——只返回字段名，绝不返回密钥值 |
| `vault_rename` | 一次调用即可重命名条目（`vault_update` 的快捷方式） |
| `vault_restore` / `vault_purge` / `vault_restore_recent` | 从回收站恢复 / 永久删除 / 撤销最近一次删除 |
| `vault_lock` / `vault_unlock` | 显式锁定保险库（清空内存密钥）/ 重新解锁 |
| `vault_password_history` | 列出条目的历史密码(1Password/Bitwarden 风格,最新在前,上限 10 条;不含当前密码) |
| `vault_password_rollback` | 将条目密码回滚到某条历史记录(当前密码先归档,可逆) |
| `vault_apply_tags` | 按查询批量增/删/替换条目标签（支持 dry-run，不含密钥） |
| `vault_totp_uri` | 为存储的或裸 TOTP 密钥生成 otpauth:// 配置 URI |
| `vault_rotation` | 报告已过期 / 待轮换 / 即将过期的凭据；`soonWindowDays`（1-90，默认 7）调整即将窗口（不含密钥） |
| `vault_health` | 保险库健康扫描：弱密码/复用凭据、未启用 2FA、不安全的 http:// 站点,以及整体安全评分（0–100） |
| `vault_watchtower` | 每个条目的风险分析(借鉴 1Password Watchtower / Bitwarden):标记弱密码、键盘序列、嵌入年份、常见密码、跨条目复用、http 站点、缺 2FA、过期——带 0–100 评分与 good/warn/poor 判定(不含机密);条目行显示 ⚠ 徽标 |
| `vault_breach_check` | Watchtower 风格泄露扫描：对接 Have I Been Pwned（k-anonymity,仅发送 SHA-1 前缀）,附离线常见密码兜底 |
| `vault_integrity` | 校验磁盘上的保险库文件可正确解密并与内存中的 store 一致 |
| `vault_merge` | 合并一个条目到另一个；`keepSource: true` 保留源条目 |
| `vault_attach` / `vault_attachments` / `vault_attachment` / `vault_detach` | 给条目附加文件（私钥、证书、配置、恢复码）——base64 存于加密条目内随库加密;列出/读取/移除 |
| `vault_quick_add` | 快速录入（标题 + 一个密钥），支持 tags/notes |
| `vault_expiry` | 设置/清除过期（`expiresAt: 0` 移除） |
| `vault_stats` | 概览计数，含 `trashCount`（不含密钥） |
| `vault_verify` | 校验单个条目或审计全部条目（`all: true`）：按 kind 检查完整性、端口/过期合理性（不含密钥） |
| `vault_duplicates` | 查找重复分组：`mode` = `both`（默认）/ `title` / `content`（不含密钥） |
| `vault_report` | 可打印清单，含到期/轮换列与统计页脚（不含密钥） |
| `vault_match_url` | 按 URL 查找匹配的登录条目（Bitwarden/1Password 风格:精确主机、子域、父域、路径前缀;www./端口归一化）,带 0–100 评分——绝不返回密码 |
| `vault_env` | 把标记 env 的条目（tags 含 `env`）渲染为 `KEY=VALUE` 行 —— 见[环境变量导出](#环境变量导出) |
| `vault_copy` | 复制条目（含密钥）到另一个命名保险库 |
| `vault_templates` | 内建 + 用户自定义模板（save/list/remove,KeePassXC 风格）;内建模板新增 Wi-Fi、服务器、数据库、身份、银行账户、银行卡（借鉴 1Password） |
| `vault_notes` | 追加或整体替换条目的自由备注（等价于一次 `vault_update`） |
| `vault_changes` | 列出某个时间窗内（默认 24 小时）的库内活动:新增/更新/软删除,最新在前 |
| `vault_find` | 模糊查找:忽略大小写、标点与空白,所以 `gh` 也能命中 `GitHub` |
| `vault_mask` | 把任意文本里像凭据的部分打码（API key、token、密码、私钥),便于安全地记录或引用 |
| `vault_history` | 本进程内的近期活动,最新在前:写操作（增/改/删/恢复/清空/合并/回滚)与读操作（读取、验证码） |
| `vault_recent` | 列出最近新增或更新的条目（最新在前),只含非敏感摘要 |
| `vault_pin` | 收藏（置顶）条目,使其在搜索与列表中排在最前 |
| `vault_unpin` | 取消收藏（移除 favorite 标记） |
| `vault_tags` | 列出全库用到的所有标签及每个标签下的条目数 |
| `vault_generate_username` | 生成随机用户名或匿名邮箱建议 |
| `vault_rotate_password` | 生成新的强密码、写入条目,并把新值返回给调用方交给目标服务 |
| `vault_touch` | 把条目标记为"最近使用"（只更新 updatedAt,不改内容） |
| `vault_note_secret` | 用一个自动生成的标题存一条单独的密钥（临时记一条,不必先想标题）|
| `vault_search_advanced` | 多条件组合搜索:标题子串、用户名/邮箱子串、kind、标签、创建时间区间、仅收藏 |
| `vault_count` | 返回活跃条目数量（可按 kind / 标签过滤） |
| `vault_set_icon` | 设置条目的 UI 图标（emoji）与强调色 |
| `vault_describe` | 用自然语言描述一条目（kind、身份、主机/URL、标签、到期),不泄露任何密钥 |
| `vault_last_modified` | 按最后修改时间列出条目,最新在前 |
| `vault_has` | 快速判断库里是否存在匹配的凭据（标题/用户名/主机子串;`exact` 时按标题精确匹配） |
| `vault_autofill_check` | 判断库里是否有可用于某 URL/主机的凭据:返回最佳匹配条目（只给用户名/邮箱,绝不给密钥）,或明确"没有" |
| `vault_undelete_all` | 把回收站里的条目全部恢复 |
| `vault_get_many` | 按 id 批量读取多个条目,且只返回请求的字段 |

### 导入导出（完整档,或自定义 → 导入导出） — 27

与其它密码管理器之间的进出、批量操作,以及 env/CSV/1Password/Bitwarden/KeePass/pass 互转。

| 工具 | 说明 |
|---|---|
| `vault_import_csv` | 从 CSV 批量导入凭据（自定义列变为 fields；`overwrite: true` 合并字段到已有条目，不再产生重复） |
| `vault_bulk_delete` | 按 query/kind/tag 或显式 ids 批量软删除条目;需 `confirm: true`（默认仅试运行）;回收站可恢复 |
| `vault_export` / `vault_import` | 整库加密备份/迁移（独立导出密码） |
| `vault_export_bitwarden` / `vault_import_bitwarden` | Bitwarden/Vaultwarden JSON 互通（完整字段映射,支持覆盖） |
| `vault_import_bitwarden_encrypted` | 解密 Bitwarden 口令保护 JSON 导出（PBKDF2/Argon2id + HKDF → AES-256-CBC + HMAC）并导入,需提供导出口令 |
| `vault_import_manager_csv` | 密码管理器 CSV 自动识别表头：Bitwarden（login_uri/login_username/…）、1Password 8、Dashlane、NordPass、Keeper、LastPass（fav/grouping/extra）；支持 dryRun 预览 |
| `vault_import_kdbx` | KeePass KDBX：3.1 与 4.x、AES-KDF 或 Argon2（RFC 9106）、AES-256-CBC 或 ChaCha20 载荷、支持 keyfile |
| `vault_export_1password` | 导出为 1Password 1PUX 归档（ZIP + export.data）,可导入 1Password 或用 vault_import_1password 再导入;条目类别映射 login/信用卡/API 凭据/服务器 |
| `vault_import_1password` / `vault_import_1pif` | 1Password 1PUX（ZIP）与旧版 1PIF 文本导出 |
| `vault_import_enpass` | Enpass JSON 导出（文件夹→标签、类型化字段、TOTP） |
| `vault_import_keepass_xml` | KeePass 2.x XML 导出（明文或 ****** 掩码值） |
| `vault_import_chrome` / `vault_import_keychain` | 从 Chrome Login Data（macOS 钥匙串 / Linux 钥匙环或 peanuts / Windows DPAPI）或 macOS 钥匙串导入密码（默认只读互联网密码 `inet`——真正用于网站登录的条目,`classes: ["genp"]` 可改读通用密码；会话缓存 + 预览,避免授权弹窗轰炸）；所有文件导入均支持 dryRun 预览 |
| `vault_import_firefox` | Firefox 配置导入（logins.json + key4.db,NSS 3DES / PBES2-AES,支持主密码） |
| `vault_export_totp` | 列出所有带 TOTP 密钥的条目及其标题与签发方标签 |
| `vault_bulk_export` | 把**全部**条目（含密钥）导出为 JSON 文件,用于审计或迁移 |
| `vault_export_browser` | 按浏览器密码管理器 CSV 格式导出登录条目（name,url,username,password） |
| `vault_migrate_keepass` | 按 KeePass 2.x 导入 CSV 格式导出（Group,Title,Username,Password,URL,Notes） |
| `vault_export_keepass_xml` | 导出为 KeePassXC 兼容的 XML 文档（KeePass 2.x schema） |
| `vault_import_browser` | 导入浏览器密码管理器 CSV（name,url,username,password,Chrome/Firefox/Edge 导出格式） |
| `vault_export_wallet` | 导出为 `pass`(标准 Unix 密码管理器）兼容的目录树:一条目一文件,首行是密码 |
| `vault_import_wallet` | 从 `pass` 目录树导入:每个 .gpg（或明文）文件成为一个条目,文件名即标题,首行是密码 |
| `vault_export_env` | 把带 `env` 标签的条目写成 .env 文件（KEY=VALUE,值做 shell 引号处理） |
| `vault_export_csv` | 把条目导出为 CSV（与 `vault_import_csv` 同格式),可按 kind 过滤 |

### 浏览器会话（完整档,或自定义 → 浏览器会话） — 8

捕获与复用 cookie 会话;需要可选的 `playwright-core`。

| 工具 | 说明 |
|---|---|
| `vault_session_open` | 在真实浏览器窗口中打开指定网址,供用户手动登录（密码、二次验证、验证码）——对禁止嵌入的网站也能捕获登录态（需要可选的 `playwright-core`） |
| `vault_session_collect` | 收集已打开浏览器会话的全部 cookie（含 HttpOnly）,保存为 `cookie` 条目 |
| `vault_session_import` | 从粘贴的 JSON cookie 数组（开发者工具导出格式）或原始 `Cookie` 头字符串保存会话 cookie——无需浏览器的替代方案 |
| `vault_session_import_file` | 导入 Netscape cookie-jar 文件（curl `-b` / wget / 浏览器扩展导出；与 `vault_session_export` 输出的格式一致） |
| `vault_session_list` | 列出已保存的登录会话、cookie 数量、已过期与 7 天内过期数量（不含值） |
| `vault_session_export` | 将会话导出为 `Cookie` 请求头、Netscape cookie-jar 文件（curl `-b`）、原始 JSON（Playwright `addCookies` 格式）或可直接运行的 Playwright 片段 |
| `vault_session_close` | 关闭已打开的浏览器登录会话（已收集的 cookie 仍保留在保险库中） |
| `vault_session_prune` | 移除已保存会话中的过期 cookie（无过期的会话 cookie 保留）;`preview: true` 只报告不修改 |

### 备份与保险库文件（完整档,或自定义 → 备份与文件） — 14

备份、切换保险库、换密钥、恢复码与系统搜索。

| 工具 | 说明 |
|---|---|
| `vault_recovery_code` / `vault_verify_recovery` / `vault_recovery_status` | 一次性保险库恢复码（1Password/Bitwarden 风格）:32 位代码仅显示一次,只保存其 SHA-256 哈希;可验证代码持有;查询是否已设置 |
| `vault_rekey` | 原地升级 scrypt KDF 参数并重加密 |
| `vault_switch` | 为当前会话切换活动保险库 |
| `vault_backup` / `vault_backup_now` | 时间戳加密备份,文件名 `<库名>-backups-YYYY-MM-DD_HH-MM-SS-<hex>.json`（库名+日期一目了然）;自动保留最近 N 份 |
| `vault_restore_backup` | 从备份还原:`mode: "merge"`（默认）把备份条目复制进当前库,出现在条目列表;`mode: "replace"` 整体覆盖（先写安全快照） |
| `vault_vault_rename` / `vault_vault_delete` | 重命名命名库（文件移动,当前会话跟随）或永久删除（default 库受保护） |
| `vault_search_system` | 在 Chrome / 钥匙串中搜索站点与用户名——绝不暴露密码 |
| `vault_backup_status` | 报告距上次备份已过去多少天（1Password 式备份提醒） |
| `vault_search_history` | 搜索时包含已软删除（回收站）的条目,并标注其删除状态 |
| `vault_backups` | 列出可用的加密备份文件（最新在前）及其绝对路径与时间戳 |

**典型用法**:把 SSH 凭据存成 `kind: ssh`(host/port/username/password 或 privateKey),让模型先 `vault_search` 找到主机、再 `vault_get` 取连接信息;`api-key`/`oauth` 条目用于 API 网关的 access/refresh token 轮换。

## 安装

dsh-vault 是一个 **bundle**(声明 `dsh.bundle` 的包):安装到 profile 后,它的 `cordis.patch.yml` 会自动插入 `vault` 插件行(按包名 `dsh-vault` 引用,主密码经 `DSH_VAULT_PASSWORD` 环境变量注入)。包内含自包含构建脚本,git 安装时会自动编译 `lib/`。

以下四种安装方式均已**端到端实测通过**(安装 → bundle 层被识别 → 插件激活且 7 个 `vault_*` 工具全部注册 → `vault_add`/`vault_get` 真实往返成功 → 卸载移除 layer):

| 方式 | 命令 | 需构建 | 需 allowBuilds |
|---|---|---|---|
| npm | `add dsh-vault` | 否(预构建 lib) | 否 |
| GitHub | `add github:Ox0400/dsh-vault#v0.1.1` | 是(prepare) | 是(首次) |
| 本地路径 | `add /绝对/路径/to/dsh-vault` | 否(链接已构建源码) | 否 |
| tarball | `add ./dsh-vault-0.1.1.tgz` | 否(预构建 lib) | 否 |

### 方式一：从 npm 安装(最省事)

```sh
dsh plugin --profile web add dsh-vault
```

npm 包自带**预构建的 `lib/` 产物**,无需 allowBuilds、无需本地编译,安装即用。启动前设置主密码:

```sh
export DSH_VAULT_PASSWORD='你的强主密码'
```

### 方式二：从 GitHub 安装(锁 tag 或 commit)

```sh
dsh plugin --profile web add github:Ox0400/dsh-vault#v0.1.1
```

git 安装拉取的是**源码**,`prepare` 脚本会在安装时构建 `lib/`。pnpm ≥10 默认阻止 git 依赖执行构建脚本。实测流程:

1. 执行 `add` 命令——会因 `allowBuilds` 失败,并打印需要放行的**精确键**(含仓库 URL 与解析后的 commit hash 的那行):

   ```text
   allowBuilds:
     dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: true
   ```

2. 把该精确键追加到 profile 的 `pnpm-workspace.yaml`(`$DSH_HOME/profiles/<name>/pnpm-workspace.yaml`):

   ```yaml
   packages:
     - .
   allowBuilds:
     dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: true
   ```

3. 重新执行 `add`——pnpm 现在会运行 `prepare` 脚本,构建 `lib/` 并完成安装。

建议**锁定 tag/commit** 再安装,避免上游推送改变安装时执行的代码。允许构建 = 允许该包的代码在你的机器上于安装时执行;只对你信任的源码授予。

### 方式三：本地路径安装到 profile

```sh
dsh plugin --profile web add /绝对/路径/to/dsh-vault
```

pnpm 将 checkout 链接进 profile;只要 `lib/` 存在(必要时先在 checkout 里执行 `pnpm build`)即被识别为 bundle。

### 方式四：本地 tarball 安装

```sh
npm pack && dsh plugin --profile web add ./dsh-vault-0.1.1.tgz
```

tarball 自带预构建 `lib/` 产物,无需构建或 allowBuilds。

`dsh plugin --profile web remove dsh-vault` 卸载(同时移除依赖与 layer)。

## 命令行

插件把保险库交给助手;自带的 `dsh-vault` 命令把**同一个保险库**交给 shell 与脚本,于是技能里的子进程可以自己取密钥,**明文永远不进模型上下文**。

### 0. 先确认:你能敲的是哪条命令?

**没安装就没有 `dsh-vault` 这个可执行文件** —— 插件是 Cordis bundle,`dsh` 也没有插件子命令注册表(启动器只解析自己的参数;`dsh plugin …` 只是转发给 pnpm,所以 `dsh web exec …` 会把参数交给 web app,报 `error: too many arguments`)。看下面的例子前,先找到你那一行:

| 你的情况 | 能敲的命令 |
|---|---|
| **源码 checkout**(`git clone` + `pnpm build`) | **`pnpm vault`** —— 或 `node lib/cli.js`、`./lib/cli.js` |
| 插件**装进了 profile** | `pnpm dsh plugin --profile web exec dsh-vault` |
| **全局安装**(`npm i -g dsh-vault`) | `dsh-vault` |
| **什么都没装** | `npx dsh-vault` |

```sh
# 源码 checkout(就是本仓库当前的状态):
pnpm vault list

# 全局安装后(npx 同理)才是:
dsh-vault list
npx dsh-vault list
```

下面的例子统一写成 `dsh-vault …`,**请替换成你那一行的写法**(checkout 用 `pnpm vault …`)。

### 1. `env` 是干什么的

`get` 取**一个**值,不需要任何标签。`dsh-vault env` 是另一种场景:脚本要**一次拿好几个**密钥。它把**打了 `env` 标签**的条目打印成 shell 赋值语句,让脚本自己取走:

```
$ dsh-vault env
DEMO_API_KEY='sk-demo-123'
OTHER_PASSWORD='pw-not-exported'
```

`eval "$(dsh-vault env)"` 就是把这些赋值读进**当前 shell** —— 这通常才是你要用它的原因:

```sh
eval "$(dsh-vault env)"          # KEY=VALUE 变成 shell 变量
echo "$DEMO_API_KEY"
```

两个前提要知道:

- **只有打了 `env` 标签的条目才会被导出。** 一个都没打时,输出的是
  `dsh-vault: nothing to export — no entry carries the "env" tag.`;先在 UI 里给条目的标签加上 `env`,或让助手执行 `vault_update { id, tags: ["env"] }`。
- 名字默认由标题 + 字段推导(`DASHSCOPE` + `apiKey` → `DASHSCOPE_API_KEY`);想固定名字就在条目的 `envKeys` 里指定 —— 见[环境变量导出](#环境变量导出)。


**命名 —— 只有三个,其中两个故意都叫 `env`:**

| 名字 | 是什么 | 含义 |
|---|---|---|
| `env` | 条目的**标签** | 决定哪些条目会被 `dsh-vault env` 导出;不打标签就不导出 |
| `env` | **命令** | 打印带该标签的条目 |
| `envKeys` | 条目的**字段**(可选) | 固定确切名字,例如 `["DASHSCOPE_API_KEY"]`;旧的单数写法 `envKey` 仍作为"只写一个"的简写接受 |

除此之外没有任何叫 `env*` 的东西:`list --json` / `show` 只报 `envKeys`(该条目会导出的名字,也就是 `get` 接受的名字)、`envTagged`(是否被 `env` 包含)与 `hasTotp`(`totp` 能否为它生成动态码)。

### 2. 有哪些命令

```sh
dsh-vault list                             # 分组显示:哪些会被 env 导出、哪些不会
#   Exported by `env` (tag "env") — 1 entry:
#     a1b2c3d4-…  api-key   DASHSCOPE        → DASHSCOPE_API_KEY
#   Not exported (no "env" tag) — 1 entry; `get <name>` still works for it:
#     9f8e7d6c-…  api-key   Example Billing  → EXAMPLE_BILLING_API_KEY
dsh-vault get my-entry                     # 取该条目的主密钥,仅 stdout
dsh-vault get my-entry --field apiKey      # 取指定字段
dsh-vault get my-entry --mask              # 只确认存在,不打印值
dsh-vault show my-entry                    # 非敏感字段的 JSON
dsh-vault totp my-entry                    # 当前的 TOTP 动态码(只有 6 位数字)
dsh-vault totp my-entry --json             # 附带 digits / period / secondsRemaining
dsh-vault env                              # 带 env 标签的条目 → KEY=VALUE
dsh-vault env --mask                       # 人类视角,两段都是 KEY=VALUE:
#   ## exported items
#   NPM_REGISTRY_API_KEY=npm_***
#   ## unexported items
#   EXAMPLE_BILLING_API_KEY=***
dsh-vault export-env .env                  # ……或写成 0600 权限的 .env 文件
dsh-vault verify                           # 校验主密码,stdout 无输出
```

密钥只走 stdout,其余(进度、错误)走 stderr,所以 `$(dsh-vault get …)` 与管道都正常;退出码 `0` 成功、`1` 运行错误、`2` 用法错误。

`totp` 是为了让脚本能自动完成两步验证、又完全不碰长期密钥:`code=$(dsh-vault totp my-entry)` 只输出六位数字。如果你用 `get my-entry --field otpSecret`,打印的是**密钥本身** —— 只有确实要把密钥导入别处时才那么做。`--json` 会多给一个 `secondsRemaining`,脚本据此等一个新窗口,而不是硬闯即将过期的码。

### 3. shell 引号:不是 `export $(…)`

正确的是 `eval "$(dsh-vault env)"`;`export $(dsh-vault env)` 是错的。命令替换会**按空白拆分**,而且**不会**去掉 CLI 输出的引号 —— 值里只要有一个空格就会被拆成多个词,所有值还会带上字面引号:

```sh
$ export $(dsh-vault env); echo "$WITH_SPACE"
['pass]                     # 在空格处被拆断
bash: export: `here': not a valid identifier
$ eval "$(dsh-vault env)"; echo "$WITH_SPACE"
[pass word here]            # 按原样读入
```

脚本里更推荐用文件形式,**完全不用 `eval`**:

```sh
dsh-vault export-env .env && set -a && . ./.env && set +a
```

### 4. 主密码

依次取 `--password-stdin` → `$DSH_VAULT_MASTER_PASSWORD`(或 `$DSH_VAULT_PASSWORD`)→ 交互式提示(**输入不回显**,终端会切到 raw mode;`tests/e2e/cli-prompt.mjs` 会验证这一点);它**不会**去读插件配置文件里的密码,所以 CLI 既不依赖、也不会泄露 profile patch 里那份:

```sh
DSH_VAULT_MASTER_PASSWORD=… pnpm vault list
printf '%s\n' "$PASSWORD" | pnpm vault list --password-stdin
```

### 5. 安装细节

**源码 checkout:**

```sh
git clone git@github.com:Ox0400/dsh-vault.git && cd dsh-vault
npm install && npm run build            # 生成 lib/,含可执行的 lib/cli.js

pnpm vault list                         # 仓库自带入口(package.json script)
node lib/cli.js list                    # 等价写法
./lib/cli.js list                       # 文件本身可执行
npm link                                # ……或把真正的 dsh-vault 放到 PATH
```

要**开发插件**并对接 profile,还需要把 checkout 软链进 profile 并加一层 patch 插入插件(本仓库平时就是这么开发的);**CLI 完全不需要这些** —— 它直接读保险库文件。手工软链的插件**没有** `.bin/dsh-vault` 垫片,`pnpm … exec` 会报 `Command "dsh-vault" not found`;要么按依赖正常安装(`pnpm dsh plugin --profile web add dsh-vault`),要么手动建垫片:

```sh
ln -sf ../dsh-vault/lib/cli.js ~/.dsh/profiles/web/node_modules/.bin/dsh-vault
```

**npm 安装:**

```sh
pnpm dsh plugin --profile web add dsh-vault          # 装进 profile(插件与 CLI 一起)
pnpm dsh plugin --profile web exec dsh-vault list    # --profile 必填

npm i -g dsh-vault && dsh-vault list                 # 全局
npx dsh-vault list                                   # 或者干脆不装
```

读命令(`list`、`get`、`env`、`show`、`totp`、`verify`、`export-env`)只依赖 Node,所以全局与 `npx` 都能直接用;**只有写保险库**才需要 harness 运行时,而插件环境一定有。

profile 这条路还有两个注意点:垫片可能被之后在该目录执行的 `pnpm install` 清掉;`pnpm dsh plugin --profile web add dsh-vault` 会依据包的 `dsh.bundle` 声明重整 `dsh.profile.bundles` —— 如果你自己的 patch 层**也**插入了这个插件,就会**挂两份**,要先删掉 patch 层那一行。

## 泄露检查的隐私边界

`vault_breach_check`(以及安全页)可以把密码拿去和 Have I Been Pwned 比对。**离开本机的只有一样东西**:

```
GET https://api.pwnedpasswords.com/range/<SHA-1(密码) 的前 5 个十六进制字符>
Add-Padding: true
```

响应会返回该前缀桶里的**全部**后缀(几百条哈希),匹配在**本地**完成 —— 完整哈希、密码原文、以及它属于哪个条目都不会离开。如果密码已经在内置常见口令表里,**根本不会发起请求**;同一前缀的重复查询会在内存里缓存一小时。

k-anonymity **没有**隐藏的东西,直说:

- **5 个字符的前缀**(20 bit,约百万分之一桶)会连同你的 IP 发给 Cloudflare;
- 能枚举候选口令的观察者,可以算出每个候选的前缀,从而看出你是否查询过那个桶 —— 这是该协议的已知局限,加请求头也解决不了;
- `Add-Padding` 只是抹掉**响应体积**这个信号(不加它,桶的响应长度会和"该密码有多常见"相关)。

如果连前缀都不想出去,就用 `online: false` —— 此时只查离线词表,覆盖率小得多,适合当筛子而不是定论。

## 强度指示

列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ = 满分**,中间按半星分档:

![强度指示的七种状态](docs/strength-stars.svg)

半星是**真正的左半实心、右半空心**,不是在旁边写个 `½`:实现是把同样三颗星叠两层、按百分比裁剪彩色层,而 `★`/`☆` 字宽一致,所以裁剪正好落在星的边界上。

| 分数 | 实心星数 | 档位 |
|---|---|---|
| 0–8 | 0 | 弱 |
| 9–24 | ½ | 弱 |
| 25–41 | 1 | 弱 |
| 42–58 | 1½ | 中 |
| 59–74 | 2 | 中 |
| 75–91 | 2½ | 强 |
| 92–100 | 3 | 很强 |

悬停可看到精确分数与评价。分数在 **host 侧**计算(列表本身永远拿不到密钥),针对条目的**密码或银行卡 PIN**;机器生成的 API Key、私钥**故意不评分**,免得所有密钥都是满分。

## 环境变量导出

打上 `env` 标签的条目可以用 `vault_env` / `vault_export_env` 导出为 `KEY=VALUE` 行，名字按各家工具链预期的字段名生成：

| 条目 | 导出的键 |
|---|---|
| 标题 `DASHSCOPE` + `apiKey` | `DASHSCOPE_API_KEY` |
| 标题 `DASHSCOPE` + `prefix: APP_` | `APP_DASHSCOPE_API_KEY` |
| `envKey: "TAVILY_TOKEN"` + `accessToken` | `TAVILY_TOKEN` |
| 同一条目的 `refreshToken` / `fields.scope` | `TAVILY_TOKEN_REFRESH_TOKEN` / `TAVILY_TOKEN_SCOPE` |

- 字段后缀遵循惯例：`apiKey → API_KEY`、`accessToken → ACCESS_TOKEN`、`refreshToken → REFRESH_TOKEN`、`privateKey → PRIVATE_KEY`、`password → PASSWORD`、`cardNumber → CARD_NUMBER`。
- 派生名字对不上脚本时，给条目设置**环境变量名**（可选字段 `envKeys`，也可用 `vault_add`/`vault_update` 写入，编辑器里用逗号分隔）：**按顺序一一对应**、且**原样使用**（不加前缀、不从标题推导）：

  | `envKeys` | 导出结果 |
  |---|---|
  | `["DASHSCOPE_API_KEY"]` | `DASHSCOPE_API_KEY=apiKey` |
  | `["GOOGLE_ACCESS_TOKEN", "GOOGLE_REFRESH_TOKEN"]` | 分别对应 `accessToken`、`refreshToken` |
  | 条目有 3 个密钥、只配 `["MY_KEY"]` | `MY_KEY`、`MY_KEY_REFRESH_TOKEN`、`MY_KEY_SCOPE` |

  每个名字必须是合法 POSIX 名（`[A-Za-z_][A-Za-z0-9_]*`）、最多 8 个、不可重复。`envKey`（单个字符串）仍作为"只配一个名字"的简写兼容，写入时会被合并进 `envKeys`。
- 自定义字段同样会导出，形如 `<BASE>_<字段>`。

> 升级提示：1.10.64 起键名从 `DASHSCOPE_APIKEY` 改为符合厂商惯例的 `DASHSCOPE_API_KEY`。若脚本依赖旧写法，给条目设置 `envKey` 固定名字即可。

## 工具分级(工具集)

插件内置 110+ 个模型工具,但**默认只注册 11 个基础工具**(`vault_list/search/get/add/update/delete/fill/clipboard/totp/generate_password/strength`),让模型的工具目录保持精简、减少 token 开销。

在 **设置 → 凭据库 → 权限 → 模型工具集** 切换,**立即生效、无需重启**,并按保险库持久化:

| 档位 | 注册内容 |
|---|---|
| **基础**(默认) | 仅核心 —— 日常增删改查/搜索/生成/验证码/填充 |
| **标准** | + 管理: 收藏、标签、图标、到期轮换、健康、重复合并、附件、模板、env 掩码 |
| **完整** | 全部,含批量导入导出、浏览器会话、备份与保险库文件操作 |
| **自定义…** | 基础 + 任选: 管理 / 导入导出 / 浏览器会话 / 备份与文件 |

也可在插件配置里写 `tools: basic|standard|full|custom`;界面选择优先,并保存在 `<vault 目录>/tools.json`(工具集是进程级设置,不随保险库变化)。

## 配置

> [!WARNING] 安全:不要把主密码明文写进配置
> profile patch(`cordis.patch.yml` / `cordis.yml`)是很容易被提交进 git、截图或进日志的配置文件 ——
> 一行明文的 `masterPassword:` 就等于泄露了整个加密保险库的钥匙。请一律使用
> `masterPasswordEnv` 并把真实密码放进进程/ shell 环境(例如 `export DSH_VAULT_PASSWORD='…'`)。
> 凭据类插件的证据化检查清单见
> [community-plugin-audit handbook](https://github.com/sandbaseai/deepseek-harness-handbook/blob/main/docs/en/security/community-plugin-audit.md)。
>
> ```yaml
> # ✗ 不要 —— 配置文件里写明文主密码
> - id: vault
>   name: dsh-vault
>   config:
>     masterPassword: 'my-secret'
> ```
>
> ```yaml
> # ✓ 应该 —— 引用环境变量,并让文件本身保持 0600
> - id: vault
>   name: dsh-vault
>   config:
>     masterPasswordEnv: DSH_VAULT_PASSWORD
> ```

| 配置项 | 说明 |
|---|---|
| `masterPassword` | 直接配置主密码（会出现在 cordis.yml 中，不推荐） |
| `masterPasswordEnv` | 环境变量名，运行时从该变量读取主密码（推荐） |
| `path` | 保险库文件路径，默认 `$DSH_HOME/vault/default.json` |
| `name` | 保险库名，用于默认路径（如 `name: work` → `$DSH_HOME/vault/work.json`） |
| `accessMode` | 模型工具的访问策略，三态：`readonly`（只读，工具与设置页的增/改/删全部被拒绝）、`ask`（默认——写入前询问，每次增/改/删都会走 harness 审批通道请你确认）、`auto`（自动读写，无需逐次确认）。设置页提供同样的三选一下拉并持久化到 `<vault 目录>/access.json`。 |
| `autoCapture` | `false`（默认）。设为 `true` 时，系统提示词会指导模型识别对话中出现的凭据，并**按用户偏好**用 `vault_add` 提供保存。 |
| `lockTimeoutSeconds` | 自动锁库：空闲超过该秒数后自动重新锁定（清空内存密钥），之后每次读写需 `vault_unlock`。`0`/缺省为禁用。 |
| `exportPasswordEnv` | 存放 `vault_export`/`vault_import` 导出密码的环境变量名（绝不能作为模型参数传入）。 |
| `backupRetention` | 保留多少个加密备份（默认 10）；`vault_backup` 自动清理更旧的副本。 |

示例：

```yaml
- id: vault
  name: dsh-vault
  config:
    masterPasswordEnv: DSH_VAULT_PASSWORD
    accessMode: ask
    autoCapture: true
```

`autoCapture: true` 时，当你在对话中分享凭据（如 "我的 npm token 是 npm_…"），助手会提议存入；你同意后立即调用 `vault_add`。`autoCapture` 关闭时，只有你明确要求才保存。设置页显示当前模式（只读 / 写入前询问 / 自动读写）可用下拉切换，还有**自动捕获开关**（检测对话中的凭据 → 提议保存）、类型筛选、健康与轮换摘要、回收站视图、以及默认遮罩的密钥字段（显示/隐藏切换）。

首次调用任一工具时自动创建保险库；之后每次启动用主密码重新解锁。**忘记主密码 = 数据永久丢失**（无后门，这是设计使然）。

## 开发

本地 clone 开发:

```sh
git clone git@github.com:Ox0400/dsh-vault.git
cd dsh-vault
pnpm install    # 安装 devDependencies(typescript/tsdown/vitest 等)
pnpm build      # 构建 host 侧 lib/*.js 与浏览器 bundle lib/client.js
pnpm test       # 运行 506 项 vitest 测试
```

> 测试需要 harness 的 `dsh-llm`/`dsh-system-prompt` 等 peer 包,在 harness monorepo 内开发时由 workspace 链接提供。

凭据列表里的品牌图标是**内联进浏览器包**的 68 条 SVG 路径 —— 没有运行时依赖,也不联网。它们是生成的,而且生成器就在仓库里:

```sh
npm i --no-save simple-icons@16      # 15 MB,仅开发用;永远不会成为本包的依赖
pnpm gen:icons                       # 重新生成 src/client/site-icons.ts
pnpm gen:icons -- --check            # 与上游不一致时退出 1
```

生成器只重写图标表;`gist.github.com`、`console.cloud.google.com` 这类**主机→品牌映射是人工整理的判断**,会原样保留。simple-icons 后来下架的品牌(`amazonaws`、`slack`、`linkedin` 等)会被保留并列出,而不是被静默丢掉。

常用命令:

```sh
# 单元 + 集成测试（vitest，506 项）
pnpm test            # 或 npx vitest run

# 类型检查
pnpm typecheck       # tsc -p tsconfig.json --noEmit

# 构建（host 侧 lib/*.js 与浏览器 bundle lib/client.js）
pnpm build           # = build:host (tsc) + build:client (tsdown)

# 打包发布（可选：npm pack 产物可直接 `dsh plugin add ./dsh-vault-0.1.1.tgz`）
npm pack
```

仓库内所有测试通过：506/506（crypto/TOTP/密码生成/store CRUD/网关/集成）。

浏览器侧检查跑在真实 `dsh web` 上（不属于 vitest），且**一律拒绝在 default 库上运行**：

```sh
node tests/e2e/theme-check.mjs      # 双主题 token、进度环轨道、附件行
node tests/e2e/polish-check.mjs     # 空态文案、相对时间悬停
node tests/e2e/contrast-audit.mjs   # 8 个标签页 × 双主题的 WCAG 对比度扫描
```

安全规则与「让对比度审计说谎的两个坑」见 `tests/e2e/README.md`。

## 不让凭据进入公开内容

本仓库的一切都是公开的 —— npm 包、GitHub release、issue 与 discussion 帖子。发布前用 `pnpm scan:secrets` 扫描工作区、被跟踪文件与提交信息中所有「像凭据」的字符串;`pnpm scan:public` 会额外扫描已经发布出去的每个 release 与评论。本地扫描也包含在 `pnpm test` 里,所以被提交进来的凭据会让测试失败,而不是被发出去。

## 主题适配

UI 不写死颜色，而是读取宿主的设计变量：本地语义层映射到真实的 `--dsw-alias-*` 命名空间，并带字面量兜底，脱离 DeepSeek Harness 也能正常渲染。

```css
--v-text: var(--dsw-alias-label-primary, #1f2328);
--v-border: var(--dsw-alias-border-l2, #d9d9d9);
--v-success-text: color-mix(in srgb, var(--v-success) 55%, var(--v-text));
```

因为别名会随 `body[data-ds-dark-theme]` 翻转，浅色/深色自动跟随宿主 —— 没有第二套样式表，也没有主题参数。

这层语义层要守住的两条规则：

- **状态色 `state-*-primary` 不能当正文颜色。** 它们是为填充块和图标调的：白底上 success 只有 2.28:1、warn 只有 2.15:1，远低于 WCAG AA。`--v-*-text` 变体把它们与主题正文色混合，于是同一条声明在浅色下压暗、深色下提亮（浅色 5.8/5.6/7.5:1，深色 12+/11+/6.5:1）。
- **不要用 `opacity` 弱化文字。** `opacity: .8` 会把 5.8:1 变成 3.9:1。要「次要」就用 token（`--v-text-2`），不要用透明度。

`tests/e2e/contrast-audit.mjs` 会在两套主题、全部标签页上验证这两条。

## 打包与发布

本包是标准 npm bundle:

- `dsh.bundle.patch` → `cordis.patch.yml`(安装到 profile 后自动应用的 layer)
- `dsh.client` → 浏览器端声明(`exports["./client"]` 指向 `lib/client.js`)
- `prepare` 脚本 → git 安装时自包含构建(`tsc` host + `tsdown` client)
- 没有运行时 `dependencies`:宿主提供的一切都走 `peerDependencies`,不会产生重复实例。
  `playwright-core` 只有浏览器登录会话用得到、且是懒加载,因此声明为**可选** peer ——
  普通安装**不会**下载它(约 13 MB)。需要那组工具时,把它装进你的 profile:
  `cd ~/.dsh/profiles/web && npm i playwright-core`

可选发布途径:

```sh
npm pack                  # 产出 tarball → dsh plugin add ./dsh-vault-0.1.1.tgz
npm publish --access public   # 发布 npm → dsh plugin add dsh-vault
```

## 安全边界与已知限制

### 它保护什么

- **离开这台机器的保险库文件** —— 备份、同步盘、被拿走的硬盘。条目用 AES-256-GCM 加密,密钥由主密码经 scrypt 派生(N=32768, r=8, p=1,参数持久化在文档里,以后可提升;旧文档仍可解密)。
- **同一台机器上的其他用户** —— 保险库文件与 CLI 写出的文件都是 `0600`。
- **秘密意外进入模型上下文** —— `vault_search` / `vault_update` 从不返回密码、密钥或令牌;CLI 的存在就是为了让脚本取密钥时不必经过一次对话。
- **把密码写进配置文件** —— 前提是你用 `masterPasswordEnv` 而不是 `masterPassword`(见上方警告)。

### 它不保护什么

- **同一个宿主进程里的其他插件。** dsh-vault 自己就是个插件:它与 harness 同进程运行,解密后的条目就在那个进程的内存里。`readonly` / `ask` / `auto` 约束的是**模型的工具调用**,不是插件之间的隔离。harness 对自己的凭据存储也是同样的说法:agent 的工具进程以你的 OS 用户身份运行,因此"无法把秘密与 agent 隔离"([`@deepseek-ai/dsh-credentials-local`](https://github.com/deepseek-ai/deepseek-harness/tree/main/packages/credentials/credentials-local))。只有把插件放到独立进程运行才能改变这一点,而这不是某个插件能自己做到的。
- **被攻陷的机器或账号** —— 键盘记录器、挂在 harness 进程上的调试器,或任何以你的身份运行的东西。
- **你自己敲进文件、截图或 shell 历史里的主密码。** 所以才要 `masterPasswordEnv`。
- **模型拿到值之后做什么。** `vault_get` 会把值放进该次工具调用的结果,也就是模型上下文;`vault_search`/`vault_update` 则从不暴露。

### 减少暴露面

`lockTimeoutSeconds` 会在空闲后自动锁库,`vault_lock` 可以随时手动锁;两者都会清空内存中的密钥。把主密钥交给操作系统钥匙串(macOS Keychain / Windows Credential Manager / libsecret)能让明文密码**不再出现在配置与环境文件里** —— 也就是"磁盘被复制后仍然存在"的那部分暴露 —— 但它**不会**造出进程内边界:以你的身份运行的插件同样可以向钥匙串索取同一个条目。DSH 项目目前把"OS 钥匙串凭据提供方"列为待做事项。

已知限制:本插件面向单机/个人部署;团队共享保险库不在范围内。

## 已收录于

- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/README.zh.md#L2869) — 官方精选目录（安全与防护分类）
- [awesome-deepseek-harness (Dominic789654)](https://github.com/Dominic789654/awesome-deepseek-harness/blob/main/README.zh-CN.md#L903) — 社区目录（安全分类）

等待维护者合并的提交：[Anil-matcha/awesome-dsh-plugin #127](https://github.com/Anil-matcha/awesome-dsh-plugin/pull/127) · [0xsline/awesome-deepseek-harness #563](https://github.com/0xsline/awesome-deepseek-harness/pull/563) · [dsh-handbook #65](https://github.com/Electricitysheep/dsh-handbook/pull/65)
