@moruteaven/dsh-key-panel
v1.0.1
Published
Managed secret store for DSH Desktop. Operator-owned keys, injected into the assistant's shell as $DSH_* variables, with a settings-page panel and three operator-controlled access modes.
Maintainers
Readme
dsh-key-panel
npm 上的 @moruteaven/dsh-key-panel · English · 简体中文 · 日本語
给 DSH Desktop 用的密钥管家。把 API 密钥集中放在一处,
以 $DSH_* 环境变量的形式注入助手的 shell,然后由你在设置页的面板里决定——
助手到底能拿它们做什么。
它解决什么问题:助手要调 Cloudflare、OpenAI,或者你的数据库,就得有凭据。 直接粘进对话,就永久留在了对话记录里;存成文件,最后又散落到某个你早已忘记的 目录。这个插件把它收进一个文件、暴露成一个环境变量,而值本身从不进入对话。
助手要部署 Worker
→ 执行:wrangler deploy --api-token $DSH_CLOUDFLARE_TOKEN
→ shell 里有,模型的上下文里没有功能
- 设置页面板。增删改查、显隐、复制,都在你管理其它设置的地方,不用改配置文件。
$DSH_*注入。每个密钥都是助手 shell 里的一个变量,改动下一条命令即生效,无需重启。- 三档权限,由你设定并持久保存:
只读(默认)·可写·可编辑。 - 平台与账号。多数服务商要两个值(账号 id + 令牌),而一个服务商常配多个账号。按平台和账号归组后,每个槽位就是
DSH_<平台>_<账号>_ID和DSH_<平台>_<账号>_KEY。归组只是存储的组织方式——变量名仍是扁平的,读它的东西也都不用变。 - 来源标记。密钥记录是你建的还是助手建的,且写入无法互相"洗白"。
- 最近活动。记录每条 shell 命令实际拿到了哪些变量,以及助手主动申报的目的。两者并排展示,从不配对。只记名字和时间戳——任何值都不会写进去。
- 名称作用域(暂未开放)。宿主支持把助手限定在
DSH_AGENT_*之类的名字下,存储里已设置的作用域也照常生效,但本版本面板上的入口是隐藏的。 - 两段式删除。删除需要第二次带令牌的确认调用——单次误调用删不掉东西。
- 值永不进模型。任何模式下,没有任何工具会把密钥值返回给模型。
- 幂等、可热重载。面板每次调用都重新读取当前权限。
安装
设置 → 插件 → 搜索 "dsh-key-panel" → 安装 → 重启 DSH Desktop之后面板出现在 设置 → 密钥。
其它渠道:
- npm ——
npm i @moruteaven/dsh-key-panel,然后在你的 profile 的package.json里把裸包名@moruteaven/dsh-key-panel加进dsh.profile.bundles。这个列表只接受裸包名,file:或路径会被拒绝(file:放在dependencies里是合法的)。 - 从源码 —— 见 CONTRIBUTING.md。
使用
添加密钥
设置 → 密钥 → 添加密钥。
| 字段 | 说明 |
| --- | --- |
| 名称 | 必须匹配 DSH_[A-Z][A-Z0-9_]*,例如 DSH_CLOUDFLARE_TOKEN |
| 用途 | 可选。会作为该变量的说明展示给助手 |
| 值 | 密钥本体。明文存储,保存后只以掩码显示 |
然后直接告诉助手用它:
部署这个 worker,token 在
$DSH_CLOUDFLARE_TOKEN里。
平台与账号
当一个服务商需要两个值(比如账号 id 和令牌),而你在它下面有不止一个账号时,平铺的列表会变得难读。用 添加平台 和 添加账号 把这些密钥归成一类,变量名由插件替你拼:
| 你填 | 得到 |
| --- | --- |
| 平台 CF、账号 WORK | DSH_CF_WORK_ID 与 DSH_CF_WORK_KEY |
提交前面板会先把两个名字显示出来,你能看到最终会落进 shell 的是什么。标识符只允许大写字母、数字和下划线;小写会被拒绝而不是悄悄转成大写——你没要求过的名字,比需要重打一次更糟。
密钥就在账号行里填。 账号建好之后,它的两个变量名已经定下来了——名字是从标识符拼出来的,不是你选的。所以账号那一行有一枚「填入密钥」按钮,点开是个弹出面板,一次收两个值,直接挂到这个账号下。弹出面板里会先把两个名字列出来,但不需要你敲——这才是重点:让你把一个插件算好的名字抄到顶部的「添加密钥」卡片、一个字段抄一次,那是誊写,不是输入。
每个槽位都显示自己有没有值,也可以先空着以后补。未分组的密钥仍然用「添加密钥」——它们没有账号可以填。
平台和账号都另有一个显示名,面板上显示的是它。它和标识符刻意分开:标识符会拼进变量名、之后不能改;显示名随时可改。如果你想让面板上写着「Cloudflare」而变量名保持 DSH_CF_*,这两个字段就是干这个的。
如果某个账号会生成的名字已经被占用——比如你在建账号之前就手工加过 DSH_CF_WORK_KEY——面板会先提示并列出涉及的密钥,等你确认。继续保存会覆盖它们原有的值,所以不会静默发生。
两点值得知道:
- 归组是组织方式,不是安全边界。 变量仍是普通的扁平
DSH_*名字——这正是关键,因为 shell 没有嵌套。归组给你的是一个好读的面板,顺带还带来一个正好对齐到单个账号的作用域(见下)。 - 删平台或账号不会带走你的密钥。 只要还有密钥挂在它下面,删除就会被拒绝,你得先清空或改挂。如果你选择强行继续,那些密钥会被退回未分组,值一概不动。
从没归过组的密钥保持原样。这个功能不要求你重组任何东西才有用,已有的存储也照常可用、无需迁移。
最近活动
面板会保留两条记录,并把它们并排放在一起:
| 记录 | 谁写的 | 内容 |
| --- | --- | --- |
| 助手申报 | 助手调用 key_panel_intent | 它自己说的时间戳、目的,以及打算用的名字 |
| 注入命令 | 插件自己,每次 shell 命令解析环境时 | 时间戳,以及那次命令实际拿到的名字 |
两者是并排展示,不是配对。列表里上下相邻的一条申报和一次注入,可能属于同一个任务,但数据本身没有任何东西这么说——把它们连起来就是把猜测当成事实呈现。
「注入命令」不意味着什么。 宿主在每条 shell 命令执行前解析整个 $DSH_* 环境,看不到命令拿它做了什么。所以一行只说明这些名字在那一刻在作用域里——不表示命令读了它们,更不表示用它们做了什么。把这份记录当作「哪些流程依赖了哪些密钥」的信号,不要当作审计凭证。
由此有两点实际影响:
- 助手可能什么都不申报。
key_panel_intent是可选的,跳过它没有任何代价,所以会有未申报的活动——这是预期内的。插件只在便宜的地方询问意图,但从不强制:每条命令前面加一道必填步骤,很快会变成肌肉记忆,也就不再携带信息。 - 开销不在命令路径上。 记录先在内存里缓冲、再批量落盘,所以记账不会拖慢任何命令。文件有上限(超出后丢弃最旧的),写失败被吞掉——这份文件丢了尾部,损失的是趋势,不是密钥。
记录与密钥库放在一起,文件名 usage.jsonl,只保存名字和时间戳。任何值都不会写进去。
权限档位
| 档位 | 助手可以 | 助手不可以 |
| --- | --- | --- |
| 只读(默认) | 使用密钥 | 改任何东西——面向模型的工具一个都没注册 |
| 可写 | 新增;覆盖自己建的 | 删除任何东西;改你建的密钥 |
| 可编辑 | 增、改、删 | ——(删除仍需二次确认) |
在面板里切换,下一次工具调用即生效,不用重启。
从「只读」开始。 只有当你确实想让助手自己添加凭据时,再往上放——后两档就是 为这一个场景存在的。
按名称限定
本版本面板上暂未开放。 作用域功能本身是完好的——校验、持久化、每次模型调用时的判定都在,存储里已设置的作用域也继续生效。隐藏的只是设置它的那个输入框(
lib/client.js里的SHOW_SCOPE_UI)。需要时可以直接在存储文件里设置;把该标志打开即可恢复入口。
在选档位之前,有个安全后果值得先知道:没有设置作用域时,限制助手能触到哪些密钥的只剩权限档位本身。 作用域未设置就等于没有限制。想要更窄的边界,就自己去设一个——目前只能写存储文件。
作用域用于收窄助手能碰的名字:
| 取值 | 效果 |
| --- | --- |
| (留空) | 不限制 |
| DSH_AGENT_* | 只允许该前缀 |
| DSH_CF_* | 只允许该平台的密钥 |
| DSH_CF_WORK_* | 只允许某一个账号的凭据 |
只支持 * 这一个通配符,表达不了路径,也表达不了正则。
配置项
| 设置 | 位置 | 默认 |
| --- | --- | --- |
| 权限档位 | 面板 | readonly |
| 名称作用域 | 仅存储(面板已隐藏) | 不限制 |
| 存储位置 | $DSH_HOME/key-panel/keys.json | ~/.dsh/key-panel/keys.json |
| 活动日志 | $DSH_HOME/key-panel/usage.jsonl | ~/.dsh/key-panel/usage.jsonl |
存储格式
{
"version": 2,
"policy": { "accessMode": "readonly", "scopePattern": null },
"platforms": {
"CF": { "label": "Cloudflare", "createdAt": 1758428400000 }
},
"accounts": {
"CF/WORK": { "platform": "CF", "identifier": "WORK", "label": "工作", "createdAt": 1758428400000 }
},
"keys": {
"DSH_CF_WORK_TOKEN": {
"value": "…",
"description": "Cloudflare Workers 部署令牌",
"origin": "operator", // "operator" | "model"
"platform": "CF", // 可选的归组字段
"account": "WORK",
"field": "key", // "id" | "key"
"createdAt": 1758428400000,
"updatedAt": 1758428400000
}
}
}写入走 临时文件 → fsync → rename。少了 fsync,崩溃可能留下一个已改名却
内容为空的文件——读回来就是「所有密钥都没了」。
version 1 的文件能原样加载:归组字段全是可选的,平台概念出现之前写入的密钥读回来 就是「未分组」。不需要迁移步骤。
usage.jsonl
活动记录放在另一个文件里,每行一个 JSON 对象:
{"t":1758428400000,"kind":"intent","names":["DSH_CF_WORK_TOKEN"],"note":"deploy staging"}
{"t":1758428450000,"kind":"use","names":["DSH_CF_WORK_TOKEN"]}| 字段 | 含义 |
| --- | --- |
| t | epoch 毫秒 |
| kind | intent(助手申报)或 use(注入命令) |
| names | 涉及的名字,已去重。永远不是值。 |
| note | 仅 intent——目的,截断到 500 字符 |
批量追加,并保留最近 1000 条,所以不会无限增长。与 keys.json 不同,它不是原子写入,也不逐行 fsync:进程被强杀可能截断最后一行,读取时跳过即可。这个取舍在这里是对的——这份文件是信号,不是密钥,为了持久性拖慢每条命令不值得。
两种记录由不同的一方在不同时刻写入,因此分开存储,展示时按时间对齐。插件从不把它们合并。
安全
密钥以明文存储。 这是刻意的取舍,完整说明(信任模型、不变量、非目标)见 SECURITY.md。
简要版:
- 任何以你的 OS 用户身份运行的程序都能读到存储文件。请当成
.env对待。 - 任何模式下,助手都无法把密钥值读进对话记录。
- 助手永远改不了权限档位和作用域——那是操作者专属的。
- 「只读」档下写入工具不存在,而不是拒绝调用。
开发
npm test # 两套一起跑 —— 427 条断言
npm run test:host # 宿主侧:权限、存储、工具、网关(298)
npm run test:client # 客户端 bundle:契约、插槽、RPC、字典(129)客户端测试按真实前端的加载方式跑 lib/client.js:伪造
window.__ModuleLoader__、require 只允许种子模块,然后断言 bundle 求值时
不产生副作用、只引用种子模块,以及面板调用的每个 RPC 端点都与宿主网关对得上。
没有构建步骤。 宿主侧是纯 ESM,浏览器侧是一个 CJS factory 字符串。
lib/
policy.js 权限档位、作用域 glob、决策函数
store.js 持久化、来源标记、原子写入
tools.js 面向模型的工具、删除确认台账
index.js 宿主侧 —— Typert 网关、shellEnv 注册
client.js 浏览器 bundle —— 设置面板本仓库要求遵守的约定、以及怎么跑测试,见 CONTRIBUTING.md。
兼容性
- DSH Desktop 2.0.11+(dsh
0.1.5-rc.1) - Node 20+
- 客户端部分仅支持 web 平台
许可
Apache License 2.0 · 署名信息见 NOTICE
选 Apache-2.0 而不是 MIT/BSD,是因为它对处理凭据的工具多了两条有意义的条款:
- 专利授权(第 3 条)。MIT 和 BSD 完全没有这一条,而它同时要求贡献者做同样承诺。
- 商标限制(第 6 条)。别人不能拿作者的名义为衍生分支背书——这正是 BSD-3-Clause 背书条款提供的同一层保护。
其余照旧宽松:可商用、可修改、可闭源分发。
