cordis-plugin-secret

DSH PLUGIN工具与能力

仓库 xinvxueyuan/cordis-plugin-secret

工具与能力 未标注验证等级 安全审查:未标注 可信度:未标注 可安装插件

这款 DeepSeek Harness 插件让 Agent 能向人类索取密钥、人类也能主动附加密钥,但 Agent 只拿到 DSH_SECRET_* 变量名,明文仅按会话注入 shell 环境。

一句话选型结论

按资料字段整理

cordis-plugin-secret 是工具与能力类的 DSH 可安装插件,未做沙盒验证,暂无安全审查结论,列出 5 项功能,没有现成安装命令,需要按仓库 README 接入。验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么;涉及密钥或系统权限的,建议先在隔离环境试装。

分类工具与能力 收录类型可安装插件 验证等级未标注验证等级 安全审查未标注
验证等级 UNKNOWN 未标注验证等级
GitHub 星标 0 复刻 0 · 未关闭 issue 未标注
最近更新 2026-10-10 创建于 2026-10-05
README 150.4 KB 正文体量,越大说明说明书写得越细

安装

可安装插件
在 DSH 里执行,装完用 dsh plugin list 确认已挂上;标了 L4 / L5 的命令已在沙盒里真实跑通
  1. 1
    确认版本与运行环境

    仓库没有标注兼容版本与 Node.js 要求,先确认你的 DSH 版本能装,再继续。

  2. 2
    执行安装命令

    在 DSH 里执行上面的命令;多包仓库要注意区分具体子包名,别装错包。

  3. 3
    确认已挂载

    用 dsh plugin list 查看列表里是否出现这个插件,出现即挂载成功。

  4. 4
    试跑并检查权限

    加载后跑一个最小用例,确认它能正常工作;同时看它申请了哪些文件、命令或网络权限。

兼容 DSH 版本未标注版本不匹配时安装会报错
Node.js 要求未标注来自仓库 package.json 的 engines 字段
前置依赖无需要同时安装的同级依赖
npm 包名未发布到 npm可用 npm view 查看包信息

能做什么

5 项功能
字段来自项目仓库与公开索引,中文说明由本站标注

cordis-plugin-secret:DeepSeek Harness 中只交付变量名的密钥授权插件

  • Agent 内联卡片索取密钥
  • 人类可主动附加密钥到消息
  • 只返回不透明变量名
  • 明文仅按会话注入 shell
  • 持久化经凭据服务落库

上游标签

ai-agentsauthorizationcordiscredentialsdeepseek-harnessdshdsh-pluginopensourcepluginsecretsecuritytypescript

验证等级与安全审查

未标注验证等级
自动化流程结论,不是人工审核
验证等级未标注验证等级上游未给出验证结论,安装前请自行确认
安全审查未标注静态分析结论,未标注不代表已通过审查
仓库健康度未标注按仓库活跃度与最近更新时间给出
可信度未标注综合仓库活跃度与验证结果

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 列出了 5 项功能,能直接看出它解决什么问题。

先想清楚的地方

  • 安全审查还没结论,装之前先看仓库改了什么、要什么权限。
  • 没有现成安装命令,要按仓库 README 手动接入。
  • 验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么。

更适合

  • 需要在工具与能力场景里给 DSH 补能力的人。
  • 能接受在本地装依赖、愿意自己确认权限的人。

不太适合

  • 只使用 DSH 官方内置能力、不接受第三方插件的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
插件验证等级安全审查星标兼容版本安装命令
cordis-plugin-secret当前 未做沙盒验证 未标注 0 未标注 无
huashu-design 未做沙盒验证 未标注 3.0 万 未标注 有
loopx 未做沙盒验证 未标注 6,233 未标注 无
agy-staff 未做沙盒验证 未标注 870 未标注 有
dsh-startup-screen 未做沙盒验证 未标注 633 未标注 有
minara-skills L4 · 沙盒安装通过 中风险 360 未标注 有

仓库档案

工具与能力
字段来自项目仓库与公开索引,中文说明由本站标注
作者 / 维护方xinvxueyuan仓库 cordis-plugin-secret · 默认分支 未标注
分类工具与能力tools
收录类型可安装插件声明了 dsh.bundle、可用 dsh plugin add 安装的条目
开发语言TypeScript仓库主语言
许可证Apache-2.0商用或二次分发前请确认条款原文
星标 / 复刻 / issue0 / 0 / 未标注收录时的数值,非实时
创建 / 更新2026-10-05 · 2026-10-10最后推送 未标注
本站收录2026-10-06首次进入本站目录的时间

README 全文

150.4 KB
作者原文,中文版优先;只做安全转义,不改内容

cordis-plugin-secret



> Cordis(DeepSeek Harness)插件:密钥在人与 Agent 之间双向流动——Agent 可以开口索取,人类也可以把一枚密钥主动附加到自己的消息上——而 Agent 永远只拿到一个不透明的变量名(如 DSH_SECRET_OPENAI)。插件自身从不把值放进工具结果、错误消息、日志、DOM 或会话记录;值只经 ctx.shellEnv 按会话注入到 shell 环境——由 Agent 自己避免回显。

  • Host 半(索取方向):注册 secret_request 工具;用 ctx.authorization 的凭据获取流程承载持久授权;用 ctx.credentials 落库;用 ctx.shellEnv 按会话注入 DSH_SECRET_*。
  • Host 半(附加方向):POST /api/secret.attach 把人类填的值暂存在进程内存(暂存不等于授权:此时不注入任何变量);携带标记 @DSH_SECRET_* 的用户消息一旦落进会话日志,session/event 就把它提升为按该条消息锚定的授权;agent/pre-step 追加一条只有变量名的说明消息,其中逐变量写明这枚标记在模型侧写作 [secret DSH_SECRET_*](正文本身不改写——harness 会让改写落盘,见下文)。
  • Client 半(索取方向):在 Agent 输出流里渲染会话流内一级卡片(conversation.chat.node,key=secret-request),无 shell.overlay 遮罩;卡片带 type="password" 输入与显示/隐藏切换,把「同意 / 拒绝 / 忽略 / 其他」四个决定、申请理由、用途说明与授权范围摆在人类眼前,并允许人类改写 Agent 请求的范围。
  • Client 半(附加方向):在输入区 conversation.input.left(紧随「访问模式 / 计划」控件组右侧)加一个切换式按钮;按下后在输入框上方(conversation.input.overlay)浮起填值胶囊;填完点「插入到光标处」,草稿光标处得到真正的内联 chip(data-composer-chip="secret",只显示变量名),可在后续 shell 取用;点击该 chip(或刷新后由 lexicon 装饰出的同名引用)会在同一浮层展开只读详情胶囊。0.3.0 起,消息旁的旁挂胶囊与(被接管后的)原胶囊都能打开详情——前者开输入框上方的信息框、后者开右侧栏详情页;信息框内另有历史记录区,@ 菜单会列出可用密钥(详见「人类主动附加密钥(反方向)→ 0.3.0 的四项增强」)。
  • 管理方向(0.4.0):注册第二个工具 secret_manage(列举 / 解绑 / 真删 / 改作用域 / 请人类改值)与 /api/secret.manage(GET 列举、POST 执行);参数集里没有 value,Agent 没有任何提交值的通道——值只能由人类在掩码输入框里键入。人类侧信息框新增「管理 / 改值 / 危险确认」三个面(详见「秘密的完整 CRUD(0.4.0)」)。
  • 传输:两个方向的对话框都经本插件自有的、位于 ctx.connection 信任栅栏内的 /api 路由与 Host 通信。密钥值只出现在 POST /api/secret.answer、POST /api/secret.attach 与(仅 action:"value" 时)POST /api/secret.manage 的请求体里,从不进入 URL / 查询串 / 会话日志 / 响应体。

安全不变量与副作用披露

安全不变量(实现并测试)

  1. 插件自身的输出永不携带明文:工具结果、错误消息、日志、事件、渲染文本、HTTP 响应体与 DOM 属性中都不含密钥值;render() 只输出变量名与元数据。单测对四种 decision 的所有字段做全量字符串扫描,断言值不出现;Client 半另有断言证明填值胶囊的值不越出那个掩码输入框。边界:值确实会按会话注入到 shell 环境(这是本插件的功能),因此"明文不进上下文"取决于 Agent 不回显 $env:DSH_SECRET_*,而不是插件的输出通道。
  2. Agent 只拿到变量名:approved 返回 { decision, variable, scope, ref, source },variable 形如 DSH_SECRET_OPENAI。
  3. 值只发给本机 Host:客户端只向 /api/secret.answer(索取方向)与 /api/secret.attach(附加方向)发起同源 POST(签名 HttpOnly Cookie + Host/Origin 栅栏),不写 URL、不写 localStorage、不打印 console。附加方向的 GET /api/secret.attached 只回传变量名/名称/范围/状态,永不回传值;0.3.0 的 POST /api/secret.adopt 同样不携带值(由宿主自己从凭据库解析),GET /api/secret.history 与 GET /api/secret.available 也只回传值无关字段。0.4.0 的 GET /api/secret.manage 只回传变量名与元数据(无值字段),POST /api/secret.manage 只在 action:"value" 时携带人类在掩码框里键入的值——它由人类提交,不经模型。
  4. 会话级密钥不落盘:scope: "session" 的值只存在于进程内存(Host 的暂存表与会话授权表)。落盘只走凭据服务,且只发生在 persistent。
  5. 持久化只经凭据服务:persistent 经 ctx.credentials.set(<变量名>, value) 写入凭据引用空间(provider 管理的可写源);同时向记录空间提交一条不含密钥材料的标记记录(索取方向 kind: "grant",附加方向 kind: "attachment",payload 只有 envVar/name/scope/authorizedAt)。绝不写自建文件,绝不在仓库里存明文。
  6. 会话边界失败关闭(见「边界处理」):锚点离开会话表面即撤销并不再注入。
  7. 人类输入密钥的控件既不可自动填充,也不可被密码管理器捕获(用户实机报出的缺陷的修复):现象:真实浏览器会在这四个密钥输入面上给出自动填充建议,已装的密码管理器扩展也会捕获它们。根因:修复前这四个字段只有 autoComplete:'off'(现代浏览器对登录类凭据字段基本忽略这个值)、没有任何厂商忽略标记、也没有别的退出开关,而掩码用的又是原生 type="password",浏览器正是据此把它当作密码域。修复:本插件渲染的文本/密码输入都带一套抑制属性——autocomplete="new-password"、autocorrect="off"、autocapitalize="off"、spellcheck=false,以及厂商忽略标记 data-1p-ignore(1Password 官方文档逐字给出)、data-lpignore(LastPass:只有其支持页的标题级引用——本环境抓取失败、其公开仓库 0 命中,如实标注为证据最弱的一条)、data-bwignore(Bitwarden:opt-in,只有用户在该扩展的设置里打开对应选项后才生效)、data-form-type="other"(Dashlane)、data-protonpass-ignore(Proton Pass:依据是厂商源码——WebClients 里 PASSWORD_MANAGER_IGNORE_PROPS 收录的正是这同一套属性,见 packages/pass/…/Field.tsx 与 applications/wallet/…/attribute.ts)。按面分层(不是"每一个 input 都一样"):4 个能持密钥明文的字段(索要卡片的值输入、附加胶囊的密钥内容、管理面「改值」、管理确认卡片的「新密钥内容」)带齐 9 键袋且另加只读直到聚焦的守卫——字段先以 readOnly 渲染(自动填充只在加载时预填,而只读字段不在候选里),聚焦时由 onFocus 释放;2 个标识符输入(凭据键、标题)带 autocomplete="off" 的同一套袋且同样带只读直到聚焦的守卫,并且自 0.4.3 起不再带 id/name、改用隐式 label 关联(包住可见标题与控件,见下)。**依据要分清归属(t21 纠偏,勿混读)**:真正有文档支撑的是 **(a) 浏览器侧的键控行为**——Chrome 团队 web.dev《Learn Forms: Autofill》逐字写明浏览器**按 `name`(某些浏览器还看 `id`)记录与回填字段**,并对"值每次都不同、不可能复用"的字段(该页给的例子是一次性验证码,凭据键与标题同属此类)**推荐 `autocomplete="off"`**;同页还写明 `off` 对**密码字段无效**("即使使用 `autocomplete="off"`,浏览器仍会提供密码自动填充选项"),这正是那四个密钥字段必须留着 `new-password` 而不能改用 `off` 的原因,而 `new-password` 用在纯文本字段上只会招来"生成强密码"的建议;以及 **(b) 五个厂商忽略标记**(1Password 官方文档逐字给出 `data-1p-ignore`;Bitwarden / Dashlane / Proton Pass 是**厂商自家源码**;LastPass 只到支持页标题级)。**而"去掉 `id`/`name`"是本次的设计选择,不是厂商文档的推荐做法**:1Password 官方文档的原意恰好相反——它建议给字段加**唯一的 `id`/`name`**,好让 1Password 能识别该字段;我们只在这两个**不持密钥**的标识符字段上反向操作,唯一目的是让浏览器不再把历史值当作可复用项、从而不再弹下拉,代价是放弃显式关联(见下面的可访问性口径)。不要把这一条读成"厂商推荐去 id";**1 个不持值的掩码展示**(`disabled`)也带袋、不带守卫;**自由文本 `textarea`** 只带 4 键卫生子集(`autocomplete`/`autocorrect`/`autocapitalize`/`spellcheck`);**2 个 scope radio** 不持值、不带袋、**且带 `name`**。所有文本/密码输入都没有 `name`,整个 UI **没有、没有 submit 按钮(每个按钮都是 type="button")。可访问性口径(0.4.3,如实记录取舍):两个标识符字段改成隐式关联后,MDN 把嵌套与 for/id 记为"等价",但同时提醒"并非所有辅助技术都实现隐式关联",并一般推荐用显式 for;因此这两个控件同时带与可见标题逐字相同的 aria-label,把可访问名固定成显式提供的那一份(可见文本与可访问名一致,满足 Label-in-Name),点击标题仍会聚焦控件(嵌套 label 的原生行为)。代价如实写明:这两个字段失去了"显式关联"这一 MDN 推荐形态,换来的是去掉浏览器与扩展据以识别字段的稳定 id;tabIndex 未改、字段仍可聚焦、可用性未牺牲其它属性(正对照与机械断言见测试)。四个密钥字段保留显式 for/id:它们靠 new-password + 厂商忽略标记 + 守卫关闭浏览器侧凭据路径,而 MDN 推荐的显式关联在这里更值得留。掩码仍用 type="password":替代方案 -webkit-text-security 是非标准属性(MDN 明写"不建议在生产使用、浏览器支持有限"),一旦被忽略就会把密钥显示成明文,比自动填充更糟。机械断言:两个 Client 测试逐一渲染这些面并断言上述属性与守卫,修复前的形态下它们全部失败(正对照见测试注释)。需真人**:具体浏览器是否还弹自动填充建议/是否还提示保存密码、各管理器扩展是否仍捕获、以及只读守卫的代价(见下一条与「已知限制」),只能在真机浏览器里确认。
  8. 明文允许存在的全部位置(穷举,仅此六处):
    - P1 填值胶囊 / 管理面「改值」面的本地输入 state(掩码输入框);
    - P2 一次 POST /api/secret.attach(附加方向)或一次 action:"value" 的 POST /api/secret.manage(改值方向)的请求体;
    - P3 Host 进程内存里的暂存记录(已登记但尚未随消息发送);
    - P4 Host 进程内存里的 GrantStore 记录(已绑定到某条消息);
    - P5 仅当人类显式选择「持久」时写入的凭据库;
    - P6 shellEnv 在执行期把值注入子进程的环境变量。

明文绝不允许出现:模型上下文(对话文本)、持久会话日志(任何事件字段)、工具结果、DOM 属性与可见文本、未发送草稿的持久化投影、URL/查询串、HTTP 响应体、异常与错误消息(一律固定文案,不复述上游)、console、以及除 P5 之外的任何落盘文件。

这条穷举是刻意的:Host 必须持有值才能在后续 shell 里注入(否则"随消息取用"不可能成立),因此本插件不声称"值不进入内存"——只声称上面这张表,并且用单测钉住它。

副作用披露(按真实实现逐条列出)

  • 会按会话把明文注入子进程环境:ctx.shellEnv.register 为每个变量名声明一个 contributor,每次 shell 执行都重新校验该执行的会话是否仍持有有效授权,有效才注入。这是插件功能本身,也是明文唯一离开本进程的出口——注入给子进程意味着该子进程写下的任何输出都可能带上它,是否回显由 Agent 负责。
  • 会在 Host 进程内存里持有明文:人类填的值先落在暂存表(attachTtlMs 到期即丢、每会话 maxAttachmentsPerSession 条上限),随消息绑定后进会话授权表;两者都在内存。进程退出即消失。
  • 可能写凭据库(仅当人类显式选「持久」):经 ctx.credentials.set(<变量名>, value) 写入凭据引用空间,并追加一条不含密钥材料的标记记录;session 范围不落盘。
  • 模型侧的改写由注记承担,且注记会落盘(按 source 去重):agent/pre-step 不改用户消息正文(harness 会让改写落盘,见「人类主动附加密钥(反方向)→ 模型侧到底看到什么」),而是追加一条只含变量名的注记,其中逐变量逐字写出「正文里的 @DSH_SECRET_* 即该变量,模型侧写作 [secret DSH_SECRET_*];它不是文件路径」。注记是 durable 的 user/message(因此在对话流里是一行注入说明),且按自身 source 去重:模型可见 surface 上已有同一条就不再追加,重复引入同一标记不会堆叠。
  • 会在会话日志里留下变量名标记:人类发送的消息本身(含 @DSH_SECRET_OPENAI 这种变量名)作为普通 user/message 事件持久化。变量名不是密钥材料,但它会长期留在日志里。
  • 会挂 10 条 /api 路由(9 个精确路径:/api/secret.manage 一条路径两种方法,单次注册、按 request.method 分派——真实注册表按精确路径建键、与 methods 无关,所以同一条路径注册两次会在装配时抛 exact Fetch route ... is already registered 并让整个插件条目不激活;0.4.0 正是这样在实机上不激活,0.4.1 修掉):/api/secret.pending(GET)、/api/secret.attached(GET)、/api/secret.attach(POST)、/api/secret.release(POST)、/api/secret.answer(POST),以及 0.3.0 新增的三条——/api/secret.history(GET,本会话历史流水)、/api/secret.available(GET,@ 菜单的可用密钥)、/api/secret.adopt(POST,把凭据库里的持久记录登记到本会话),以及 0.4.0 新增的 manage(GET 管理面列表 / POST 执行一个管理动作)。全部位于 ctx.connection 的信任栅栏内(本机 / 可信 Host、同源标记、签名浏览器 Cookie)。值只出现在 attach、answer 与(仅 action:"value" 时)manage 的请求体里;其余路由都不回传值,adopt 的取值也由宿主自己经 credentials.resolve 完成,值不跨线。
  • 会注册客户端座位与一个引用源:conversation.chat.node(key secret-request)、tool.call.toolview(key secret_request 的 null 占位)、conversation.input.left(id secret-attach-toggle)、conversation.input.overlay(id secret-attach-capsule),外加一个名为 secret 的 InputTriggerSource 与 locale 命名空间 secretAttach。0.3.0 起再增加三处增量注册:conversation.chat.node(key sr-chip,消息旁的旁挂胶囊)、sidebarRightTabs 的 secret-attach-detail 类型,以及它的正文座位 sidebar.right.pane.tab(key @xinvxueyuan/cordis-plugin-secret/secret-attach-detail)。该页签的标题不另注册座位:注册表走 fallback 取类型自己声明的 title(address),即变量名(src/client/entry.ts:3555),所以用户看到的结果是对的。tool.call.toolview 只替换本插件自己那次调用的泛型工具行,不触碰别的工具;其余都是增量座位。0.4.0 再增加两处增量注册:conversation.chat.node(key sr-manage,Agent 侧的管理确认卡)与 tool.call.toolview(key secret_manage 的 null 占位);同样只影响本插件自己那次调用,且管理面的三个新面都在既有胶囊组件里,不新增座位、不新增引用源,dsh.client.inject 也不变。0.5.0 再增加一处增量注册:conversation.input.right(id secret-paste-composer,输入区的「粘贴」动作,见「0.5.0 的四项」第 2 条);仍然只增座位、不新增引用源,dsh.client.inject 不变。0.6.0 该座位的用途变更:座位保留,但按用户裁定删掉了里面的「粘贴」按钮,改为 t46 粘贴接管提示的渲染位(见「0.6.0 的修复与需求变更」第 4、5 条);座位名与 id 未再变,dsh.client.inject 仍不变。
  • 不做的事:插件自身不 spawn 子进程、不读写仓库文件、不发起网络请求(除被 Harness 自己的 API 通道承载的上面列出的 10 条同源路由外),也没有任何遥测。

安装

# 在已加载的 Harness 中(危险全权限)
plugin_manager action=install_bundle target=<包名 | 本包绝对路径>

安装做两件事:

  1. 用 pnpm 把本包写进当前 profile 的依赖(本地目录会成为 link:<绝对路径>),并把包名追加到 profile package.json 的 dsh.profile.bundles;
  2. 从已安装包内的 dsh.bundle.patch(即本包的 cordis.patch.yml)加载 patch 层,Host 端热加载。

profile 的组合树是「root 空清单 → dsh.profile.bundles 里每个 bundle 的 patch → profile 的 cordis.patch.yml → --patch 覆盖」逐层拼出来的,所以 profile 的 cordis.patch.yml 里看不到 secret 行是正常的——那一行来自本包。本包的 cordis.patch.yml 是:

- insert:
- id: secret
name: '@xinvxueyuan/cordis-plugin-secret'
inject: [tools, authorization, credentials, shellEnv, sessions, agents, connection]
config:
requestTimeoutMs: 300000
maxPendingRequests: 4

(attachTtlMs、maxAttachmentsPerSession、maxHistoryPerSession、maxAvailableEntries 未在这里显式写出,取下面配置表的默认值。)

安装结果的 application 为 applied 表示本次变更已生效;warnings 会说明 Client 半是否需要刷新页面。

注意:对同一个 link: 依赖重复按绝对路径安装是 no-op,此时安装器报 changed: false + application: "failed" + error.code: "ambiguous-install"——它按 profile 依赖的 diff 归属安装目标,而路径 spec 与已存在的同名依赖对不上。这不代表插件没生效(该次调用没有改动任何文件);用 cordis_inspect_query 核对更可靠:host Tool/listTools 应出现 secret_request,host Config/listConfigs 应出现 include:secret,client Slots/listSubTree {root:"conversation.chat.node"} 的 occupants 应出现 secret-request(若同时看 {root:"shell.overlay"},判据是那里不应再有 secret.request.dialog)。

来源构建:npm install && npm run build(lib/ 是 loader 与浏览器实际加载的产物;src/ 为 TypeScript 源码)。

组合节点(官方)

Host 半声明 7 个硬依赖。Loader 会让这一行等待这些服务:缺任何一个时插件保持未 apply(不抛错),在该 profile 里表现为「不出现」而不是「出现但坏了」。

服务键 官方提供方(row / 包) 必需性 用途
tools tools / @deepseek-ai/dsh-tools 硬 注册 secret_request 与 secret_manage 两个工具。
authorization authorization / @deepseek-ai/dsh-authorization 硬 用官方的凭据获取流程承载持久授权(每个 key 一次 attempt)。
credentials credentials / @deepseek-ai/dsh-credentials-local 硬 persistent 范围的落库与回读;记录空间里那条不含密钥材料的标记也写在这里。
shellEnv shell-env / @deepseek-ai/dsh-shell-env 硬 为每个变量名声明 contributor,按会话、按执行解析 DSH_SECRET_*。
sessions session / @deepseek-ai/dsh-session 硬 ctx.sessions.get(id) 把授权锚定到活会话,并校验锚点仍在会话表面上。
agents agent / @deepseek-ai/dsh-agent 硬 ctx.agents.get/roots 把调用归到发起它的 Agent(live root / delegated / 失效 id)。
connection connection / @deepseek-ai/dsh-client-connection(dsh-web-app 层) 硬 在官方信任栅栏内挂 /api/secret.* 路由(本机 / 可信 Host、同源、签名 Cookie)。
llm llm / @deepseek-ai/dsh-llm 可选 附加密钥时让模型建议一个变量键名;缺 llm 时插件仍激活、附加仍成功,只是退回本地规则命名。

llm 故意不进 inject:它经 ctx.get('llm') 可选获取(src/index.ts 的注释写明了这一点),把它写进硬依赖会让没有 llm 的 profile 直接失去整个插件。

一致性约束:patch 的 inject: 与 src/index.ts 的 export const inject 是同一份依赖名单的两个声明处——前者决定 Loader 等哪些服务才激活该行,后者决定插件自己何时 apply。两处必须集合相等,改一处而漏另一处会造成「行已激活但插件不 apply」或反之。test/manifest.test.ts 从这两个文件各自的文本里提取列表并比对,不硬编码名单;它随 npm test 一起跑。

配置(Config)

字段 默认 说明
requestTimeoutMs 300000 单个 secret_request 等待人工确认的上限(也是工具 timeoutMs 的基础,工具自身总在上层超时前返回)。
maxPendingRequests 4 同时等待人工确认的授权请求数上限,超出返回 TOO_MANY_PENDING。
attachTtlMs 1800000 人类已填入、但从未随消息发送的附加项在内存里保留多久(30 分钟)后被丢弃。暂存从不等于授权:等待期间不注入任何变量,这个上界正是"值不会在长命进程里无限期滞留"的保证。
maxAttachmentsPerSession 8 单个会话同时可持有的已登记附加项上限。
maxHistoryPerSession 32 单个会话保留的历史记录条数上限(内存态,超出丢最旧)。
maxAvailableEntries 32 @ 菜单里「凭据库(持久)」一类最多列出的条目数(每项都要回读一次记录,因此有上限)。

六个键都必须是正整数:schema 的 .default() 之外,assertConfig 再手工兜一层(非正整数直接抛错)。

工具

secret_request

参数 必填 说明
name ✅ 凭据键:小写 kebab/snake(^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$),如 openai、openai-key。决定默认变量名 DSH_SECRET_。
label ✅ 对话框中的人类可读标题。
reason ✅ 你需要它的原因,原样展示给要授权的人,也是对方同意的那份理由。
scope ✅ session 或 persistent。Agent 显式选择;对话框会突出显示,人类可改。
description ❌ 补充说明,展示在对话框。
envVar ❌ 覆盖变量名,必须形如 DSH_SECRET_OPENAI,不得占用 DSH_HOME/DSH_SHELL/DSH_SESSION_ID/DSH_PROFILE/DSH_PROFILE_DIR。

返回(JSON;decision 四选一):

decision 形状
approved { decision:'approved', variable:'DSH_SECRET_X', scope, ref:{space,name}, expiresAt?, source:'store'\|'entered', notice? }
rejected { decision:'rejected', reason? } —— 人类拒绝:立即停止,不得重试。
ignored { decision:'ignored' } —— 本次未授权:可稍后再问。
other { decision:'other', text } —— 人类给了自由文本指示:按文本执行。

ref.space 说明该名字在哪儿可用:credential-ref = 可用 ctx.credentials.resolve(<变量名>) 解析(persistent);session-shell-env = 仅通过 DSH_* 注入(session)。
source = store(凭据库里已有的值)或 entered(本次人工输入)。notice 是附加诊断(例如"上一次授权所锚定的事件已不在当前会话表面上"),永不含密钥。

失败(抛错,作为工具错误结果返回,同样不含密钥):BAD_REQUEST、CALLER_NOT_LIVE、DELEGATED_CALLER、NO_SESSION、NO_ANCHOR、TOO_MANY_PENDING、TIMEOUT、AUTHORIZATION_FAILED、AUTHORIZATION_CANCELLED、STORE_READ_FAILED、STORE_EMPTY。

其中两条会把上游细节收敛掉、只报固定的本插件文案(凭据后端不是本插件能控制的组件,它的错误文本可能引用路径、引用名或其他敏感材料,因此一律不透传):STORE_READ_FAILED(凭据库读取 describe/resolve 失败 →「凭据库读取失败;细节已省略」)与 AUTHORIZATION_FAILED(授权流程失败 →「凭据授权流程失败(上游细节已省略)」)。AUTHORIZATION_FAILED 仍会带上本插件自己的二次脱敏层(把该次对话已收集到的值替换掉),措辞本身从不被当作防泄漏的唯一手段。

授权卡片(Client 半)

一次 secret_request 调用在会话流里只呈现一个交互面:它自己的会话流内卡片,渲染在 Agent 的输出流中(conversation.chat.node,key = secret-request,由本插件的 ConversationNodeDefinition 在 tool/call 事件上生成节点)。不再有全视口遮罩:卡片是普通文档流里的带边框卡片,不 position: fixed、不 inset: 0、不铺满视口、不劫持整屏 pointer events,也不占用输入区。

卡片在每个"工作步骤展示"档位(简洁 / 标准 / 详细 / 完全展开)都可见:节点不带 Turn/Step 坐标(location = { kind: 'session' }),因此 Harness 会把它作为流的根条目输出,既不会落进「已调用工具」步骤进程分组,也不会被该分组在任何档位下折叠或隐藏。同一 key 上注册的 tool.call.toolview 占位组件返回 null,用来替换该工具的泛型工具行,避免同一次调用出现两个面。
(附带说明:步骤进程分组的标题仍会把这次调用计入工具计数,并可能显示参数里的 description/name 文本——这是 Harness 对所有工具共有的既有行为,不是第二个交互面,也不含明文值。)

卡片内容,自上而下(与请求态一致;结算后折叠为摘要,可展开只读回看):

  1. 标题(label)+ 右侧状态:「准备中… / 等待你的决定 / 连接宿主失败,重试中… / 已提交… / 已授权 · 变量名 · 范围 / 已拒绝 / 已忽略 / 其他指示 / 出错 · 错误码」,以及「展开 / 收起」;
  2. 用途(reason,原样展示,左侧竖线引用块)+ description(如有);
  3. 保存方式:持久保存到凭据库 / 仅本次会话有效 两个单选(各带一句含义说明),并用加粗文字写明"Agent 请求的范围:"与"当前选择:";若人类改动范围,追加一行"你已把范围改为:…";
  4. 密钥值输入(type="password"、可切「显示 / 隐藏」);若该凭据已存在于凭据库则不显示输入框,只显示"已配置,本次仅需决定是否授权本次使用";
  5. 四个决定:同意 / 拒绝 / 忽略 / 其他(其他 切换出自由文本域 + 提交指示 + 返回);按钮带 title 说明拒绝与忽略的后果;
  6. 页脚说明:密钥明文不会交给模型,代理只会拿到变量名(并列出变量名)。

假过期缺陷已修复(客户端):卡片只把成功且可解析的 pending 响应当作事实来源。网络失败、非 2xx、响应体无法解析一律记为"暂时无法连接宿主,正在重试"——表单保持可见,按 1.2s→8s 退避重试,绝不判定为已失效、绝不关窗。只有"HTTP 成功且本次调用的 callId 确实不在等待列表中"才收束,且已提交后永不判过期(转入"已提交,等待 Agent 继续…",等待工具结果)。未提交时也只降级为卡片内的只读提示,卡片本身保留到这次调用结束。提交得到 HTTP 409 表示宿主已不再等待这份请求,同样只在卡片内说明,不再重复提交。

兜底(卡片构建失败 / 状态缺失时请求仍可见且可回答):

  • 调用参数无法解析:仍然生成节点,卡片用 Host 的权威视图渲染完整表单;
  • Host 暂不可达:表单可见,按钮禁用并就地提示,连接恢复即可提交;
  • 结算载荷缺失(例如旧日志、嵌套调用):显示"已结束(未记录结算细节)",保留用途与范围,不回退为空白;
  • tool.call.toolview 占位未生效:泛型工具行会重新出现(多一个面),卡片仍在且可答。

结算状态来自 tool/result.meta 里的值无关载荷(由工具的 output.presentationMeta 持久化,模型不可见),因此刷新页面/重放会话后同一张卡片可以从日志重建,不解析任何散文。

样式只用主题 token(--dsw-alias-*、--dsw-radius-*、--dsw-font-*),不 import 任何 Harness Client 包,浅色/深色主题都跟随 Host。

诊断面:Client 半在 globalThis.__cordisSecretClient 暴露一个只读、Object.freeze 的测试缝(纯函数、定义与占位组件;不含交互态、不含任何值),供测试与诊断使用。

人类主动附加密钥(反方向)

0.2.0 的缺陷与本版修复(0.2.1)

0.2.0 已发布,但这条链路当时不可用。缺陷有两条,0.2.1 一并修复:

  1. 附加秘密后,后续 shell 取不到变量:标记从未被提升为 grant,shellEnv 里因此没有这个变量(GET /api/secret.attached 会一直停在 staged,不会变成 bound)。
  2. 对话流里那条消息没有变量名胶囊:用户气泡显示原始文本,而不是"只显示变量名"的 chip。

根因一句话:0.2.0 让 agent/pre-step 去改写用户消息正文(@DSH_SECRET_* → [secret DSH_SECRET_*]),并假定"改写只作用于本次模型请求、不落盘";但这条链路里 agent/pre-step 的返回值就是被原样 append 落盘的那一条(dsh-agent-loop/lib/index.js:1061 直接 append 成 user/message,同一步的模型请求由同一份 surface 派生,:1262),正文被换掉之后 session/event 的绑定再也看不到 @DSH_SECRET_* 标记(绑定永不发生),气泡也因为正文里没有 @ 标记而不再渲染胶囊。

0.2.1 的修复:不再改写正文——日志、对话流与模型请求里的正文都是人类客户端发出的原始形态 @DSH_SECRET_*(session/event 因此重新看得见标记:绑定、anchorSeq、shellEnv contributor 全部成立;projectUserText 也重新把它投影成"只显示变量名"的胶囊)。模型侧的对应关系改由一条值无关的注记逐变量逐字承担(见下「模型侧到底看到什么」)。同时把提升次序固定为「先声明 contributor → 再落 grant → 最后消费暂存项」(见「绑定与失效」第 5 条)。

形态

部件 座位 说明
入口按钮 conversation.input.left 切换式(aria-pressed 与 aria-label 随状态变化),形状对齐 dsh-vision-router 的 VisionModeToggle(list 槽 {name, id, order, inject(sessionId)} + 模块级 store 承载活状态)。位置如实记录:它落在 [访问模式][计划] 这一组控件的右侧(.modes 组之后)。「夹在访问模式与计划之间」用公开座位做不到——那两个座位同在一个由 shell 渲染的 .modes div 内,除非整体接管 conversation.input.permission 这个 single 座位并自己重绘权限控件,属于脆弱做法,已否决。
填值胶囊 / 详情胶囊 conversation.input.overlay 同一座位、同一条目、两态渲染;空闲态 return null(槽位常驻)。浮在输入卡片上方(bottom:calc(100% + 4px)),与 @ 菜单同一几何,所以不遮挡正在输入的那一行。
内联胶囊 草稿 真正的 chip:ReferenceChipNode,DOM 为 ``,只显示变量名。
对话框那条消息上的胶囊 会话流 由 Harness 自己的 projectUserText 把文本里的 @DSH_SECRET_OPENAI 渲染成只显示变量名的胶囊。

参照对照(与 dsh-vision-router 的 VisionModeToggle):相同——list 槽 + id + order + inject(sessionId) 的注册形状;活状态放模块级 store 而非注册选项;按钮是新插件的增量贡献(不替换任何既有座位)。不同——座位是 conversation.input.left(用户要求紧邻「访问模式」,而参照在尾随侧的 input.right);本插件注册 locale 命名空间但有字面量兜底表(参照硬依赖 ctx.locale);本插件同时注册一个 InputTriggerSource(参照不需要)。

真 chip 是怎么来的(非 hack、非 monkey-patch)

Harness 的公开面 InputActions 故意不含引用插入(Command-style handles … stay InputBar-private),但同一份冻结契约里有一个 scoped bail 事件正是为此而设:

  • 事件:slash/input-insert-reference(dsh-client-ui-conversation/lib/types/client/contract/input.d.ts,@mode bail),载荷 { reference, span };
  • 会话作用域的监听者由 shell 自己注册(lib/client.js:14264:actx.on('slash/input-insert-reference', (req) => shell.insertReference(req.reference, req.span) ? true : void 0));
  • 该契约动词的原文就是 Replace the trigger span with one reference chip (span-CAS'd);
  • shipped 的 @ 菜单自己就走这条路(dsh-client-ui-input-trigger/lib/client.js:724)。

插件因此:注册一个 InputTriggerSource(name: 'secret',codec 决定模型形态,lexicon 让刷新后的纯文本标记仍被装饰成引用,openReference 接住点击,不实现 matchSpace/matchEnter、不产出任何候选),再用 ctx.inject(['sessions'], (scoped) => scoped.sessions.scope(sessionId)) 拿到该会话的 ctx 并发这个事件,span 取自公开动作 inputActions.captureInsertion()。

客户端半的三个可选协作方(locale / inputTriggers / sessions)一律经 cordis 的可选座位 ctx.inject([...], cb) 取得,不写进 inject、也不直接读 ctx.:cordis 的 ctx Proxy 对未声明服务抛 cannot get property "" without inject,直接在 apply() 里读会让整条 client entry failed、整页 web boot 报 Failed to load plugins。缺任何一个时按上述阶梯降级(没有 sessions 就从 L3 起步),不抛。inject 声明仍是 ['slots', 'uiConversation']。

兜底阶梯(每一级都可被测试观测):L1 上面的 chip 事件 → L3 inputActions.insertText('@DSH_SECRET_X', span)(纯文本,由 lexicon 装饰为可点击引用,DOM data-composer-text-ref)→ L4 在胶囊里显示变量名请人类手动输入。原定案里的 L2(toggleSource + pick)在实施中判定为冗余并移除:那条 pick 管线的终点就是 L1 这同一个事件、且传的是同一个 span,因此它不可能在 L1 失败的地方成功。只有 L1 成功才在 UI 与文档里说"真 chip"。

模型侧到底看到什么(逐字)

位置 内容
会话日志(durable) user/message · 请用 @DSH_SECRET_OPENAI 跑测试
用户气泡 请用 + 胶囊(只显示 DSH_SECRET_OPENAI) + 跑测试,其后一行注入说明(header 标注 producer secret-attach)
模型请求(agent/pre-step) 请用 @DSH_SECRET_OPENAI 跑测试(与日志逐字相同),其后追加一条值无关说明:- DSH_SECRET_OPENAI · 仅本次会话有效、正文里的 @DSH_SECRET_OPENAI 即该变量,模型侧写作 [secret DSH_SECRET_OPENAI];它不是文件路径。、取用方式:PowerShell 用 $env:DSH_SECRET_OPENAI,POSIX shell 用 "$DSH_SECRET_OPENAI"、「不要把该标记当作文件路径读取。」

为什么模型侧不是改写正文,而是由注记逐变量写明对应关系(这是与早期设计稿不同的一点,原因在 harness 契约,不在取舍):

  • agent/pre-step 返回的消息就是落盘的那条:loop 把它原样 append 成 user/message(dsh-agent-loop/lib/index.js:1061,surfaceOp: 'append'),而同一步的模型请求由同一份 surface 派生(:1262)。所以"改写只作用于本次模型请求、不落盘"在这个接线下不可能成立:改写必然落盘,落盘就必然改变用户气泡。
  • 模型输入按契约是会话日志的纯函数:loop 构造的请求被 deep-freeze,llm/stream 的监听者「read it, never rewrite it」(dsh-llm/lib/types/index.d.ts:37-45)。
  • 唯一能保留"仅模型可见副本"的机制是 surface replacement / message projection,但它必须 append 在目标事件之后,而 loop 的 append(:1061)与请求构造(:1262)之间没有任何可插入点;由插件自己先 append 目标再替换,会把用户消息挪到本步 system/message 提交之前(模型会先读用户消息再读系统提示),代价大于收益。自定义事件类型还需要 ignorable 才不被持久化读取拒绝(dsh-session-persistence/lib/index.js:184),树外插件拿不到。

因此日志与用户气泡保留人类客户端发出的原始形态 @DSH_SECRET_*(这样 projectUserText 才能把它投影成"只显示变量名"的胶囊),模型侧的改写改由注记承担:注记逐变量逐字写出「正文里的 @VAR 即该变量,模型侧写作 [secret VAR];它不是文件路径。」——[secret VAR] 形态因此逐字出现在模型可见文本里,且与正文的对应关系是确定的、不依赖启发式。注记是文本的纯函数(只认 @DSH_SECRET_* 的形状),刷新、重放与 fork 拿到的模型文本一致。

注记的落盘与去重:注记本身是一条 durable 的 user/message(source.kind = 'secret-attach'),所以它在对话流里显示为一行注入说明;同一条注记按自身 source 去重(src/inject.ts 的 noteVisible:模型可见 surface 上已有完全相同的 source 就不再追加),同一步/后续步重复引入同一标记不会堆叠第二份。

自己复测(活体,约 3 分钟)

> 先重启。 dsh 在进程启动时加载插件的 lib/ 构建,所以发布 0.2.1 之后必须重启 dsh web 才会加载修复后的产物;重启前的活体复测仍会复现 0.2.0 的现象(staged 不变 bound、shell 里没有变量),那是预期,不是修复失败。同理,发布 0.3.0 之后也必须重启 dsh web 才会加载四项增强的产物——重启前看不到消息旁的旁挂胶囊、信息框里的「历史记录」区、草稿移除后的自动撤销,@ 菜单也不会有新分组,这些都属预期(下面是这四项各自的复测方式)。发布 0.4.0 之后同样必须重启 dsh web 才会加载管理面——重启前 secret_manage 工具不会出现在工具列表里,信息框里也没有五条管理路径,历史里不会有 updated/scope-changed/unbound/deleted 四类事件;活体上仍是 0.3.0 的四个方向,这属预期,不是发布失败。0.4.0 是个例外:它虽然在 GitHub Release 与 GitHub Packages 上发了,但在真实运行时整个插件条目不激活(dsh: warning: 1 entry did not activate,见下面「0.4.0 的发布级缺陷与 0.4.1 修复」),所以重启也换不来可用的管理面;必须重启到 0.4.1。发布 0.4.2 之后同样必须重启 dsh web 才会加载密钥输入的抑制属性——重启前浏览器仍会照旧给那四个字段自动填充建议、密码管理器扩展仍会捕获(这就是 0.4.2 修的那件事),这属预期,不是修复失败。发布 0.4.3 之后也必须重启 dsh web 才会加载两个标识符字段(凭据键、标题)的抑制与隐式关联改动——重启前这两个字段仍会弹浏览器的历史值/自动填充建议(这正是 0.4.3 修的那件事),这也属预期。

  1. 在输入区点「附加密钥」按钮 → 胶囊里填名称(如 openai)与值,作用域保持默认「仅本次会话」→ 插入 → 输入框出现 @DSH_SECRET_OPENAI 胶囊。
  2. 发送前:在同一浏览器(同源、带签名 cookie)打开 GET /api/secret.attached?sessionId=$DSH_SESSION_ID → 该条的 state 必须是 "staged",且此刻 shell 里没有这个变量(未发送永不注入)。
  3. 发送后立刻再查同一路由 → 该条的 state 变成 "bound"(不再停留在 staged)。这是判别"绑定是否真的发生"的最快手段,也就是这一轮修复的核心判据。
  4. 让 Agent 在后续执行里只报存在性与长度(绝不回显值),例如 PowerShell:if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)" } else { "absent" }。看到 present 即「附加 → 绑定 → 注入」端到端可用。
  5. 回退/编辑重写那条消息后再查:锚点离开 live surface ⇒ 授权撤销、变量重新不可用(revoked-anchor)。

绑定与失效

  1. POST /api/secret.attach 只暂存(默认 scope: "session",可切 "persistent")。暂存不是授权:GrantStore 看不见它,shellEnv 里没有这个变量,未发送就永不注入。暂存有 TTL(默认 30 分钟)与会话结束两道兜底。
  2. 携带标记的用户消息一旦成为 durable 事件,session/event 钩子把它提升为授权,anchorSeq = 该条 user/message 事件的 seq。该服务的契约原文是 "Seed events never publish on session/event"(dsh-session/lib/index.js:1271-1275),所以重放或恢复一份历史日志不可能重新绑定——这比"回头扫日志找锚点"更稳,因为锚点 seq 本来也只有消息落盘之后才可知。
  3. 之后 shellEnv 才在每次执行时按会话注入。回退/编辑重写掉那条消息 ⇒ 锚点离开 live surface ⇒ 自动 revoked-anchor,无需额外记账。
  4. POST /api/secret.release 只能丢弃尚未绑定的暂存项;已绑定项会如实回答 state:"bound"(唯一诚实的解除方式是回退那条消息)。
  5. 提升的次序是先向 shellEnv 声明 contributor → 再落 grant → 最后消费暂存项(src/attach.ts 的 bindStaged)。声明失败时会话原样不变(无 grant、暂存项仍 armed,可在下一次标记到达时重试);反向顺序会留下「有 grant、无 contributor、暂存未消费」的静默不注入状态。另一方向无害:grant 已落但 contributor 因无关原因缺失时,resolve 查不到值 ⇒ 不注入(fail-closed)。
  6. Host 重启后暂存与授权都消失(都在内存里),日志里的标记仍在但不会自动重新武装——故意 fail-closed:没有人类在场的新进程里静默恢复一次授权,不是本插件愿意做的事。

0.3.0 的四项增强(历史胶囊点击 / 历史聚合 / 移除即撤销 / @ 菜单)

四项增强各一句:

增强 一句话
历史胶囊点击 插件在每条带标记的消息旁新增一行胶囊(旁挂节点),点击打开输入框上方的信息框;同时注册一个右侧栏文档查看器,接管 harness 渲染的原胶囊,点它时在右侧栏打开详情页。
信息框历史聚合 信息框内新增「历史记录」区,集中列出本会话的附加/授权流水(变量名、来源、作用域、状态、时间、锚点),默认全部展示(含已失效/已回退),用状态文案区分。
移除标记即撤销 草稿里的标记消失约 0.6s 后,对应的暂存记录被撤销:GET /api/secret.attached 不再把它报为 staged,会话级的值随即不可找回(重插须重填)。已绑定的记录不受本机制影响。
@ 引用菜单 输入 @ 列出本会话当前可用与凭据库已持久化的密钥,条目上用 section 标来源、description 标作用域;本会话可用的条目直接插入,凭据库条目先确认再登记(值由宿主自己从凭据库读取,不经过对话)。

两个入口的区别(同一份信息,两处入口)

你点的东西 它是什么 打开哪里
旁挂胶囊(插件在你的消息旁新增的那一行) 本插件自己的 ConversationNodeDefinition 节点(conversation.chat.node,key = sr-chip),锚在同一条消息的 seq 上、location = { kind: 'session' },因此排在用户气泡之后、且永不折进「已调用工具」分组 输入框上方的信息框(与草稿 chip 的点击是同一个面)
原胶囊(harness 渲染在你消息正文里的那个) dsh-client-ui-primitives 的 projectUserText 产物,data-ref-chip="file",点击被硬编码为 openFile('DSH_SECRET_X') 右侧栏的详情页(由本插件注册的 secret-attach-detail 类型接管),内容与信息框同源

旁挂胶囊的落位不是"大概齐":节点 key 形如 ${kind.length}:${kind}${id},同锚点、同 rank 时由 key.localeCompare 定序(dsh-client-ui-chat/lib/client.js:8273-8281)。人类消息的 definition kind 是 input-message(13 字符 ⇒ 键前缀 13:;chat:9264),而 sr-chip 是 7 字符(⇒ 7:);字典序下 7: 排在 13: 之后。实测(node -e,与 localeCompare 同语义):长度 10/11/12 的 kind 会排到 13:input-message… 之前;而该键是整体字典序比较,长度前缀与 kind 文本共同决定顺序,因此「某几个长度区间一律排在后面」并不成立(实测反例:长度 1 的 kind、以及长度 13 且文本排在 input-message 之前的 kind,都排到气泡之前)——kind 的位置必须逐个实测。本插件实际采用的 sr-chip(7 字符)已机器证明排在气泡之后。(早前版本里"长度 4 到 9 与 13 及以上一律排在之后"的说法不成立,特此更正。)

原胶囊的接管是「长度竞争」,不是契约(如实披露)

原胶囊的点击最终走到右栏的 tab 注册表:openFile → fileAddressFor → ctx.sidebarRight.openResource('dsh-resource://file/session//DSH_SECRET_X') → claim(address)。该注册表按 priority 档 → 命中的 pattern 长度 → 注册顺序 排序(dsh-client-ui-sidebar-right/lib/client.js:8782-8798)。

  • 本插件声明 priority: 'extension'、patterns: ['dsh-resource://file/**/DSH_SECRET_*'](实测长度 35),并用 canOpen 只接受最后一段形如 DSH_SECRET_* 的地址(不是我们的地址一律让给对方);
  • 本机已装的 dsh-better-sidebar 0.24.1 在同一档声明兜底 dsh-resource://file/**(实测长度 22):同档时更长的 pattern 胜出,因此这次打开归我们。

可争用性(机制固有,不是缺陷):任何后来者只要在同一 extension 档声明更长的 pattern(例如 dsh-resource://file/**/DSH_SECRET_*/**),就会静默抢走这次打开——用户会落到那个查看器,而不是我们的详情页,且我们收不到任何通知。这一点无法用契约保证,只能靠"我们当前最长"这一事实。若某天发现点原胶囊又开出别的东西(甚至退回"文件不存在"),原因就在这里。

历史记录的保留语义(如实)

维度 事实
存储 Host 侧 HistoryStore 是纯进程内存,按会话隔离,有容量上限(maxHistoryPerSession,默认 32),读出时最新在前
刷新页面 仍在(Host 进程没变)
宿主重启 / 插件重载 清空(历史不在磁盘上)
重放 / 分叉会话 不重建:Host 不读 session log 还原历史;fork 出来的子会话从空历史开始
条目来源 staged(附加登记,可含"取代了同变量的上一条")/ bound(提升为授权,带锚点 seq)/ discarded / withdrawn(草稿移除撤销)/ revoked(锚点消失,读取时懒观测)/ expired(TTL 到期)/ authorized(secret_request 索取方向的授权),并同时标 attach / request
锚点 已绑定的条目带 anchorSeq(即那条 user/message 的 seq);未绑定或未落盘的条目没有锚点,UI 不编造

客户端的四种诚实状态(都出现过,都不编造):未读(「正在读取本会话的记录…」)/读失败(「历史暂不可用」,并保留上一次的答案,不清空)/本进程无记录(「本进程内暂无记录。」)/该变量无记录(「该变量在本进程内没有记录。」)。

「默认全部展示」的落地:已失效/已回退/已丢弃/已过期都列出,并用状态文案区分——已登记 / 已绑定到消息 / 已丢弃 / 已随草稿移除撤销 / 已随消息回退失效 / 已过期 / 经授权生效。其中 revoked(已随消息回退失效)只在有人读过该变量状态之后才出现(懒观测):没观测到就不写进流水,宁缺勿假。

从草稿移除标记即撤销(含边界)

只看"草稿里没有标记"是不够的,因为发送也会清空草稿(乐观清空):普通发送的提交相位全程是 plain(不走 submitting),但 pendingSubmission 回显会同步先行出现,且它的文本就是将要发送的文本。因此撤销要在全部条件成立时才触发:

  1. 草稿里没有该标记(InputState.draft + MARKER_RE);
  2. 没有任何 pendingSubmission 的文本携带该标记(⇒ 发送中不撤销);
  3. 提交相位是 plain;
  4. 该变量曾在草稿里出现过(否则刚 attach 完还没插进去就会被立刻误撤销);
  5. 本地状态是 staged。

满足后去抖 600ms,再做一次同样的判定,然后调 POST /api/secret.release(带 reason: "withdrawn",与人类手点「丢弃」的 discarded 在历史里区分开)。

边界 行为
删掉后 600ms 内又插回(含剪切/粘贴) 不撤销,记录保留
删掉后隔一会儿再插回 记录已撤销,@ 菜单不再列出该变量;重新附加必须重新填值(会话级的值不可找回;persistent 作用域的可以从凭据库条目重新登记)
发送(Enter / 发送按钮) 不撤销:回显携带标记 ⇒ 判定中止;消息落盘后该记录提升为 bound
发送失败(草稿被恢复) 标记回到草稿 ⇒ 不撤销,记录保持 staged
发送中又手动删掉标记 同样不撤销(人类删的是草稿,不是那条已经发出的消息)
已 bound / 已 Grant 的记录 本机制不碰:只有回退/改写那条消息才会让它失效(revoked-anchor,与既有一致)
宿主未提供草稿观测能力 胶囊里显示「宿主未提供草稿观测能力,自动撤销不可用(可手动丢弃或等 TTL 过期)。」——宁可不撤销,也不误撤销
刷新页面 草稿恢复为纯文本 @VAR,MARKER_RE 仍命中 ⇒ 不撤销
撤销后重新附加同一凭据键 新记录带新的代次;任何在途的旧计时器因代次不匹配而作废,不会杀掉新记录

@ 菜单的两类来源

来源(section) 含义 条目 description 选中后的行为
本会话可用 本会话已登记(staged)或已绑定(bound)的变量 本会话 · <作用域> · <状态> 直接插入引用标记:与既有 codec / serializeReference 完全一致(插入的就是 @DSH_SECRET_X),发送后照常绑定与注入
凭据库(持久) 本插件在凭据库里留过持久记录的变量,且本会话当前没有 凭据库 · 持久保存到凭据库 · 尚未用于本会话 不直接插入:先弹一步确认(变量名 + 来源 + 作用域),确认后调 POST /api/secret.adopt,由宿主自己 credentials.resolve 取值并登记到本会话,成功之后才插入标记;失败则不插入任何东西,只显示固定文案

条目形态用足菜单的呈现能力:label 是主显示文本,但不总是变量名——「凭据库(持久)」一类是变量名(src/service.ts:664-668,记录里没有人类标题,变量名是唯一诚实的标签),「本会话可用」一类是人类标题(在胶囊里填的标题;留空即回退为凭据键,src/client/entry.ts:2808,attach 请求缺省 label ?? name)⇒ 带标题的本会话条目上不会显示变量名(变量名出现在插入后的草稿标记与胶囊详情里,那两处都按变量名显示;菜单条目的 description 只写来源/作用域/状态)。name = 凭据键(灰色别名 + 搜索键)、description = 来源·作用域·状态、section = 来源分组(不把来源塞进名字里)。菜单不渲染 hint 字段,所以来源/作用域不放在 hint。「凭据库」一类只列本插件提交过记录的密钥:凭据服务的 reference 半边没有枚举面,别处写进去的环境变量/.env 条目无法被发现,这一点如实说明而不是猜。

怎么复测这四项(先重启 dsh web)

下表左列是已在代码与 0.3.0 产物层面机器验证过的机制(lib/ 里可机械复核),右列是仍未验证、必须真人在浏览器/真实凭据库里确认的部分。两者不要混读。

增强 机制(机器已验证的判据) 仍需真人现场确认
历史胶囊点击 旁挂节点 sr-chip:uiConversation.events.register + conversation.chat.node 座位(键 sr-chip)都在 lib/client/entry.js 里;点击 → setAttachMode({kind:'detail'}),与草稿胶囊共用同一个信息框。右栏 secret-attach-detail 类型以 priority: 'extension' + dsh-resource://file/**/DSH_SECRET_* 注册,canOpen 只收末段形如 DSH_SECRET_* 的地址 旁挂行在气泡之后的视觉位置与四档展示;点旁挂行是否真在输入框上方开出信息框;点转录里的原胶囊是否真开出右栏详情页(而不是「文件不存在」),以及有没有被别的插件抢走这次打开
信息框历史聚合 信息框头部「历史记录」链接 → history 面;行有 data-secret-history-count / data-secret-history-row[data-status];Host 侧 GET /api/secret.history?sessionId=$DSH_SESSION_ID → {ok:true, entries},逐字段重建、无 value 视觉排版与长流水是否截断;四种诚实状态(未读/读失败/本进程无记录/该变量无记录)在现场是否可读
移除即撤销 插入标记 → 从草稿删掉标记 → 约 0.6s 后 POST /api/secret.release(reason:"withdrawn")⇒ GET /api/secret.attached 不再报 staged,/api/secret.history 多一条 withdrawn;重新插入不会把值找回来 真实编辑器里的时序(删掉→插回是否在 600ms 内、Enter 发送是否不触发撤销、发送失败恢复草稿是否仍为 staged)
@ 菜单 输入 @ → GET /api/secret.available?sessionId=…;两类条目用 section 分组(本会话可用 / 凭据库(持久))、description 写作用域与状态;凭据库条目选中后先确认,确认才 POST /api/secret.adopt(body 只有 {sessionId, variable}),成功之后才插入 @VAR 菜单宽度是否截断条目;带自定义标题时条目显示标题(不是变量名)在现场是否可接受

Shell 侧的可执行判据(只报存在性与长度,绝不回显值):

# 先重启 dsh web,再重跑一遍「自己复测」五步;下面是四项增强各自的检查点:
Invoke-RestMethod "http://127.0.0.1:/api/secret.history?sessionId=$env:DSH_SESSION_ID"   # 历史聚合
Invoke-RestMethod "http://127.0.0.1:/api/secret.attached?sessionId=$env:DSH_SESSION_ID"  # 移除标记后不再有 staged
Invoke-RestMethod "http://127.0.0.1:/api/secret.available?sessionId=$env:DSH_SESSION_ID" # @ 菜单的两类来源
if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)" } else { "absent" }

0.5.0 的四项(已随 0.5.0 发布)

> 版本说明(已发布):package.json 已是 0.5.0,v0.5.0 已于 2026-10-09 发布上线(tag 对象 e4240f88da8b51267b9263444009d6452d7a07d3、Release run 37971532160 / Publish run 37971532271;npm 侧已入队待 2FA 批准,详见「npm 发布」一节的 registry 现状)。
> 本节示例里的密钥值一律是假值(形如 sk-EXAMPLE-…),不是任何真实凭据。

1. 密钥键改为可选 + 附加时的会话外 AI 命名(R1)

  • 契约放宽:secret_attach 的 name 由必填变为可选。空或缺失 ⇒ 由 Host 自动命名(旧客户端与直接调用同样得 200);非空但非法的键照旧走原来的错误分支被拒(ATTACH_KEY_RE)。
  • 命名是「会话外」调用:经 llm.stream({ purpose: 'session-title', … }),不传 sessionId、不传 tools、不 append 任何会话事件;提示词只带标题与值形态(长度 / 字符集 / 类别三类 token,绝不含明文)。
  • 有界且不阻塞发送:deadline 1500ms;超时、模型答案不合法、profile 里没有 llm,一律回落到本地兜底键(标题 slug → 形态默认 → 冲突时加 -2…-99 去重)。有「模型挂起也不拖住 attach」的单测(elapsed < 500ms)。
  • llm 不在硬依赖里:用 ctx.get('llm') 可选获取(不进 inject 硬依赖),所以没有 llm 的 profile 插件仍激活、attach 仍成功(有正对照测试)。
  • 未实现(如实):定案里「晚到结果润色显示标题」这一条没有实现。原因:键在插入胶囊(即报文标记)之前就已定型,不存在一个「不改报文」的安全作用点可以回填标题;因此这是不做,而不是"待办"。

2. 输入框右侧「粘贴」动作 + 值输入框粘贴命中即转胶囊(R2 / R3)

README 较长,此处只展示前 60 KB。完整内容请到仓库查看 ↗。

常见问题

6 问
答案由本站依据条目字段整理,不引用上游原文
cordis-plugin-secret 怎么安装?

本站没有抓到该条目的安装命令,请到仓库主页确认安装方式;这类条目多数是生态应用或尚未发布安装包的项目。

cordis-plugin-secret 的验证等级是什么意思?

未标注验证等级。上游未给出验证结论,安装前请自行确认

cordis-plugin-secret 安全吗?

未标注。

cordis-plugin-secret 装之前要先确认什么?

先看它会读写哪些文件、执行哪些命令、连接哪些网络地址;验证等级只说明装得上、跑得起来,不等于安全。涉及密钥或系统权限的,建议先在隔离环境试装。

cordis-plugin-secret 用的是什么许可证?

上游标注为 Apache-2.0。商用或二次分发前请到仓库确认许可证原文。

cordis-plugin-secret 有哪些功能?

Agent 内联卡片索取密钥、人类可主动附加密钥到消息、只返回不透明变量名、明文仅按会话注入 shell、持久化经凭据服务落库。

工具与能力
按星标排序,可作替代方案

huashu-design

huashu-design

在 DeepSeek Harness 里说一句话,就能拿回可交付的设计:网页、App 原型、可编辑 PPTX、动画与信息图,自带 84 种风

未标注验证等级 3.0 万 星

loopx

loopx

LoopX 是面向长周期 Agent 的本地优先控制平面,作为 DSH plugin 为 DeepSeek Harness 提供持久目标、证

未标注验证等级 6,233 星

agy-staff

agy-staff

把 Google Antigravity CLI 作为快速 Gemini 员工接入 DSH,提供子代理侧边栏、编排模式与多种角色技能。

未标注验证等级 870 星

dsh-startup-screen

dsh-startup-screen

DeepSeek Harness 的启动界面与启动动画插件。

未标注验证等级 633 星

dsh-blender-plugin

dsh-blender-plugin

让 AI 模型通过直连 TCP 通道实时驱动 Blender 的 DSH plugin:15 个工具覆盖视口取帧、任意角度渲染、内循环搜索、

未标注验证等级 211 星

xinvxueyuan 的其它插件

2 个

插件的元信息来自作者仓库;分类、验证等级与安全结论的中文说明为中文整理,最近核验 —。