@xinvxueyuan/cordis-plugin-secret 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  4. **会话级密钥不落盘**:`scope: "session"` 的值只存在于进程内存(Host 的暂存表与会话授权表)。落盘只走凭据服务,且只发生在 `persistent`。
24
24
  5. **持久化只经凭据服务**:`persistent` 经 `ctx.credentials.set(<变量名>, value)` 写入凭据引用空间(provider 管理的可写源);同时向记录空间提交一条**不含密钥材料**的标记记录(索取方向 `kind: "grant"`,附加方向 `kind: "attachment"`,payload 只有 `envVar/name/scope/authorizedAt`)。绝不写自建文件,绝不在仓库里存明文。
25
25
  6. **会话边界失败关闭**(见「边界处理」):锚点离开会话表面即撤销并不再注入。
26
- 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 个标识符输入**(凭据键、标签)与**1 个不持值的掩码展示**(`disabled`)也带袋,但不带守卫;**自由文本 `textarea`** 只带 4 键卫生子集(`autocomplete`/`autocorrect`/`autocapitalize`/`spellcheck`);**2 个 scope radio** 不持值、不带袋、**且带 `name`**。8 个文本/密码字段都没有 `name`,整个 UI **没有 `<form>`、没有 submit 按钮**(每个按钮都是 `type="button"`)。掩码**仍用 `type="password"`**:替代方案 `-webkit-text-security` 是非标准属性(MDN 明写"不建议在生产使用、浏览器支持有限"),一旦被忽略就会把密钥显示成明文,比自动填充更糟。机械断言:两个 Client 测试逐一渲染这些面并断言上述属性与守卫,修复前的形态下它们**全部失败**(正对照见测试注释)。**需真人**:具体浏览器是否还弹自动填充建议/是否还提示保存密码、各管理器扩展是否仍捕获、以及只读守卫的代价(见下一条与「已知限制」),只能在真机浏览器里确认。
26
+ 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 关联**(`<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 **没有 `<form>`、没有 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 测试逐一渲染这些面并断言上述属性与守卫,修复前的形态下它们**全部失败**(正对照见测试注释)。**需真人**:具体浏览器是否还弹自动填充建议/是否还提示保存密码、各管理器扩展是否仍捕获、以及只读守卫的代价(见下一条与「已知限制」),只能在真机浏览器里确认。
27
27
  8. **明文允许存在的全部位置(穷举,仅此六处)**:
28
28
  - P1 填值胶囊 / 管理面「改值」面的本地输入 state(掩码输入框);
29
29
  - P2 一次 `POST /api/secret.attach`(附加方向)或一次 `action:"value"` 的 `POST /api/secret.manage`(改值方向)的请求体;
@@ -44,7 +44,7 @@
44
44
  - **模型侧的改写由注记承担,且注记会落盘(按 source 去重)**:`agent/pre-step` **不改**用户消息正文(harness 会让改写落盘,见「人类主动附加密钥(反方向)→ 模型侧到底看到什么」),而是追加一条只含变量名的注记,其中**逐变量逐字**写出「正文里的 `@DSH_SECRET_*` 即该变量,模型侧写作 `[secret DSH_SECRET_*]`;它不是文件路径」。注记是 durable 的 `user/message`(因此在对话流里是一行注入说明),且**按自身 source 去重**:模型可见 surface 上已有同一条就不再追加,重复引入同一标记不会堆叠。
45
45
  - **会在会话日志里留下变量名标记**:人类发送的消息本身(含 `@DSH_SECRET_OPENAI` 这种**变量名**)作为普通 `user/message` 事件持久化。变量名不是密钥材料,但它会长期留在日志里。
46
46
  - **会挂 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` 完成,值不跨线。
47
- - **会注册客户端座位与一个引用源**:`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` 也不变。
47
+ - **会注册客户端座位与一个引用源**:`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` 不变。
48
48
  - **不做的事**:插件自身不 spawn 子进程、不读写仓库文件、不发起网络请求(除被 Harness 自己的 API 通道承载的上面列出的 10 条同源路由外),也没有任何遥测。
49
49
 
50
50
  ## 安装
@@ -210,7 +210,7 @@ Harness 的公开面 `InputActions` **故意不含**引用插入(`Command-styl
210
210
 
211
211
  ### 自己复测(活体,约 3 分钟)
212
212
 
213
- > **先重启。** 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 修的那件事),这属预期,不是修复失败。
213
+ > **先重启。** 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 修的那件事),这也属预期。
214
214
 
215
215
  1. 在输入区点「附加密钥」按钮 → 胶囊里填名称(如 `openai`)与值,作用域保持默认「仅本次会话」→ 插入 → 输入框出现 `@DSH_SECRET_OPENAI` 胶囊。
216
216
  2. **发送前**:在同一浏览器(同源、带签名 cookie)打开 `GET /api/secret.attached?sessionId=$DSH_SESSION_ID` → 该条的 `state` 必须是 `"staged"`,且此刻 shell 里没有这个变量(未发送永不注入)。
@@ -325,6 +325,41 @@ Invoke-RestMethod "http://127.0.0.1:<port>/api/secret.available?sessionId=$env:D
325
325
  if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)" } else { "absent" }
326
326
  ```
327
327
 
328
+ ### 0.5.0 的四项(本轮,将随 0.5.0 发布)
329
+
330
+ > **版本说明**:截至本轮开发段,`package.json` 仍是 **0.4.3**,**0.5.0 尚未 bump、也未发布**;本节的「0.5.0」指本轮这些特性**将随 0.5.0 发布**。发布事实与 registry 现状只在「npm 发布」一节按已发布版本写。
331
+ > 本节示例里的密钥值一律是**假值**(形如 `sk-EXAMPLE-…`),不是任何真实凭据。
332
+
333
+ #### 1. 密钥键改为可选 + 附加时的会话外 AI 命名(R1)
334
+
335
+ - **契约放宽**:`secret_attach` 的 `name` 由必填变为**可选**。空或缺失 ⇒ 由 Host 自动命名(旧客户端与直接调用同样得 200);**非空但非法**的键照旧走原来的错误分支被拒(`ATTACH_KEY_RE`)。
336
+ - **命名是「会话外」调用**:经 `llm.stream({ purpose: 'session-title', … })`,**不传 `sessionId`、不传 `tools`、不 append 任何会话事件**;提示词只带标题与**值形态**(长度 / 字符集 / 类别三类 token,**绝不含明文**)。
337
+ - **有界且不阻塞发送**:deadline **1500ms**;超时、模型答案不合法、profile 里没有 llm,一律回落到**本地兜底键**(标题 slug → 形态默认 → 冲突时加 `-2…-99` 去重)。有「模型挂起也不拖住 attach」的单测(`elapsed < 500ms`)。
338
+ - **llm 不在硬依赖里**:用 `ctx.get('llm')` **可选获取**(不进 `inject` 硬依赖),所以没有 llm 的 profile **插件仍激活、attach 仍成功**(有正对照测试)。
339
+ - **未实现(如实)**:定案里「**晚到结果润色显示标题**」这一条**没有实现**。原因:键在插入胶囊(即报文标记)**之前**就已定型,不存在一个「不改报文」的安全作用点可以回填标题;因此这是**不做**,而不是"待办"。
340
+
341
+ #### 2. 输入框右侧「粘贴」动作 + 值输入框粘贴命中即转胶囊(R2 / R3)
342
+
343
+ - **覆盖面(我方控件,共 7 个可编辑字段)**:附加面板的标题 / 凭据键 / 密钥内容、管理面的改值、管理确认卡的「新密钥内容」、索要卡片的值 / 「其他指示」多行框,各有一个 suffix **「粘贴」**按钮:`type="button"`、`aria-label` 与可见文本同为「粘贴」、`mousedown` 被 `preventDefault`(**不抢焦点**),点击的**最后一步把焦点交还该字段**。剪贴板不可用 / 被拒 / 为空时给固定**双语**文案并引导手动粘贴,**不静默失败、不抛未捕获异常**。
344
+ - **写入与校验同路**:粘贴文本 `trim()` 后调用**该字段自己 `onChange` 用的同一个 setter**(不 `dispatchEvent`、不直接改 DOM),因此既有校验照旧生效——例如粘进凭据键的非法值仍在提交时走 `ATTACH_KEY_RE` → `badKey`,而不是绕过校验。
345
+ - **R3(仅值输入框,且为真登记)**:只有**胶囊的「密钥内容」**这一个值输入框在粘贴时做**机械判定**(`src/privacy.ts` 的规则集:厂商前缀 / JWT / PEM / 长且字符集集中 / 信息熵阈值,外加 URL·路径·邮箱·裸域名·CJK·多行·标识符等排除项;纯函数、不调模型);命中即走该表单**同一条 attach 注册路径**(真登记:同一校验、同一 `POST /api/secret.attach`),成功后字段清空并按既有行为切到详情面;未命中则按普通文本写入(原生粘贴路径**不接管**)。
346
+ - **边界(如实)**:改值面、管理确认卡(`secret_manage`)与索要卡片(`/api/secret.answer`)**不做**自动登记——对它们做会变成「假登记」。`@DSH_SECRET_*` / `[secret …]` / `dsh-resource://` 这类**引用文本**在分类器**第一步**就被排除,绝不会被当成明文再登记一次。任何失败路径(剪贴板读不到、登记被拒、网络不可达)都把粘进来的文本**放回字段**,不丢内容。
347
+ - **composer 侧(R2 的输入区半边)**:在官方座位 `conversation.input.right` 放一个「粘贴」动作,点击经 `InputActions.insertText(text, captureInsertion())` **插入到光标处**,走编辑器自身的 onChange/校验。**能力边界(如实)**:① 只能 `captureInsertion` / `insertText`,**没有写受控 value 的能力**;② 只能在官方座位放自己的按钮,**第三方插件自建的输入框不在覆盖范围内**;③ 无 caret 或编辑器拒绝插入时给手动粘贴提示,不抛错、不抢焦点。
348
+
349
+ #### 3. 胶囊 ✕ = 本会话解绑(R4)
350
+
351
+ - **语义**:✕ = **解绑**(`staged` → release、`bound` → manage `unbind`),即**从本会话移除**;**凭据库里的持久记录永不触碰**。其文案与 `aria-label` 全文**不含「删除」二字**(真删仍只在危险面、需 `confirm:true`)。
352
+ - **会话级后果(诚实结果)**:解绑是会话级的,所以同一变量的**所有**消息旁胶囊都会消失——这由「胶囊状态从实时有效性推导」保证,是**设计结果而非缺陷**。
353
+ - **bound 的 ✕ 不弹二次确认**:宿主 `src/service.ts:1050-1053` 明写该路由上「人的点击本身就是确认」;需要二次确认的只有真删那一档。
354
+ - **显隐**:默认隐藏,`:hover` / `:focus-within` 显示,`@media (hover:none)` 下常显;`aria-label` 形如 `移除 @DSH_SECRET_X`(**不含明文**)。
355
+ - **历史缺陷与修复(如实留痕)**:**0.4.x 的胶囊行在真实页面会把原始 i18n key 渲染成文案与 `aria-label`**(行内翻译绑到了 chat namespace,取不到本插件的键)——也就是**那一版该行的可访问名是坏的**、读屏会念出 key 而不是「移除 …」。**0.5.0 用 `rowT` 探测回落修好**:取不到本插件的键就回落自带字面表,不再渲染 key。
356
+
357
+ #### 4. 输入区选区「转为密钥」(R5)
358
+
359
+ - **行为以按下时刻为准**:按下时用 `captureInsertion()` 的 `start !== end` 判断"有选区";选中文本经 `useInput` 的 `draft` + `occurrences` 由 detect→clipboard 换算得到。**任何不一致都退回原「附密钥」行为**(不猜、不登记、不改写草稿)。
360
+ - **默认作用域 = `session`**:附加面板与转换路径**共用同一个常量** `DEFAULT_ATTACH_SCOPE`(`session`),即**仅本会话内存有效**;空键 / 空标题交给 R1 的自动命名。
361
+ - **实时文案是契约外手段(重要)**:公开 API 只能**按下时**读取选区、**没有选区变化通知**,所以按钮的实时文案依赖**契约外的 DOM `selectionchange` 监听**。失效模式、退回行为与「需真人确认」见「边界与已知限制 → 已知限制」里那一条;**行为永不依赖它**。
362
+
328
363
  ### 已知限制(如实记录)
329
364
 
330
365
  - **对话框那条消息上的胶囊是 `data-ref-chip="file"`**,`openFile('DSH_SECRET_OPENAI')` 仍是它唯一的点击路径。**0.3.0 起的现状**:插件已注册 `sidebarRightTabs` 的 `secret-attach-detail` 类型**接管这次打开**,所以点它会开出**右侧栏的详情页**(见「0.3.0 的四项增强 → 原胶囊的接管是长度竞争」);0.2.x 的行为是打开一个不存在的文件(无害但不正确;不泄露任何东西——变量名本来就在那里可见),该行为作为历史如实保留。**注意这条接管是"当前 pattern 最长"的长度竞争结果,不是契约。**原因(点击目标为何无法从插件侧直接改写):用户气泡的正文由 `dsh-client-ui-primitives` 的 `projectUserText` 独家渲染,它对 `@token` 形状**硬编码**为文件引用并挂 `openFile`;ui-chat 的消息管线没有可注册的引用种类(能接住点击的 `openReference` 只作用于**草稿编辑器**,不作用于转录气泡)。
@@ -408,7 +443,7 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
408
443
 
409
444
  **不可恢复性与副作用(逐字告知)**:引用空间的值被原子重写掉(`writeFileAtomic` + 0600),**没有任何副本**,也无法从会话日志反推(日志里只有变量名);记录空间的标记一并删除,因此**其它会话再也 adopt 不到它**。想再用同一变量,只能重新走 `secret_request` 或重新附加,**重新输入新值**。删除后刷新页面,「管理」面(每次都向 Host 现问)里该行消失,历史里多一条 `deleted`(仅本进程内可见)。若某个会话此前已把它绑到某条消息上,那个会话的内存副本仍在,直到消息回退或会话结束。
410
445
 
411
- ### 怎么复测 0.4.2(先重启 `dsh web`)
446
+ ### 怎么复测 0.4.3(先重启 `dsh web`)
412
447
 
413
448
  0. **重启后先看启动日志**(这是 0.4.0 缺陷的判据,也是本版修复的判据):`dsh` 的启动输出里**不应再有** `warning: 1 entry did not activate`,也不应再出现 `secret (@xinvxueyuan/cordis-plugin-secret): Error: connection: exact Fetch route "/api/secret.manage" is already registered`;工具列表里应当有 `secret_manage`,管理面可用。(0.4.0 在实机上正是这条 warning;`lib/routes.js` 里 `/api/secret.manage` 现在只注册一次、`methods` 是 `['GET','POST']`,可以离线核对。)
414
449
 
@@ -416,9 +451,9 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
416
451
  ```sh
417
452
  npm run typecheck && npm test
418
453
  ```
419
- 共 8 个测试文件 / 128 个用例。管理面在 `test/register.test.ts`:两个工具的参数集(逐字断言没有 `value`)、`list`/`unbind`/`delete`/`scope`/`value` 五个动作、五条人类侧路径、降级两个按钮(文案与线上动作都不同)、真删两档(缺 `confirm` 零副作用)、列表两个方向的 `origin` 与委派子代理的可达性注意(含根代理的正对照);**0.4.1 起还有实施真实注册表规则的严格注册表**——它断言 9 条精确路径各注册一次、`/api/secret.manage` 一行两方法,并在修复前的形态上以**同一条** `already registered` 错误失败(这就是 0.4.0 漏网的那个门);**0.4.2 起新增两条密钥输入抑制用例**(`test/client-card.test.ts` / `test/client-attach.test.ts`)——它们逐面枚举渲染出的字段并断言那一套抑制属性与只读守卫,在修复前的形态上逐条列出缺口(正对照)。四类新历史事件在 `test/history.test.ts`。`npm test` 若挂住,按仓库红线处理:**先查泄漏**(本插件自己的 teardown/定时器),临时排障才用 `node --test --test-force-exit`,**不得按进程名清场**。
454
+ 共 8 个测试文件 / **171 个用例**(这两个数字来自 0.5.0 开发段本人亲跑的 `npm test` 原始输出:`tests 171 / pass 171 / fail 0`,自然退出;见「开发」一节)。管理面在 `test/register.test.ts`:两个工具的参数集(逐字断言没有 `value`)、`list`/`unbind`/`delete`/`scope`/`value` 五个动作、五条人类侧路径、降级两个按钮(文案与线上动作都不同)、真删两档(缺 `confirm` 零副作用)、列表两个方向的 `origin` 与委派子代理的可达性注意(含根代理的正对照);**0.4.1 起还有实施真实注册表规则的严格注册表**——它断言 9 条精确路径各注册一次、`/api/secret.manage` 一行两方法,并在修复前的形态上以**同一条** `already registered` 错误失败(这就是 0.4.0 漏网的那个门);**0.4.2 起新增两条密钥输入抑制用例**(`test/client-card.test.ts` / `test/client-attach.test.ts`)——它们逐面枚举渲染出的字段并断言那一套抑制属性与只读守卫,在修复前的形态上逐条列出缺口(正对照);**0.4.3 起再加一条标识符字段用例**——断言凭据键/标题用 `autocomplete="off"`、无稳定 `id`/`name`、隐式 label 关联 + 同文本 `aria-label`、带只读守卫,并在 0.4.2 的形态上失败(正对照)。四类新历史事件在 `test/history.test.ts`。`npm test` 若挂住,按仓库红线处理:**先查泄漏**(本插件自己的 teardown/定时器),临时排障才用 `node --test --test-force-exit`,**不得按进程名清场**。
420
455
  2. **活体(需真人;管理面每次都向 Host 现问,改完刷新即可再核对)**:按上面「人类侧信息框(五条路径)」逐条点一遍——列举(两个分区读得清)、改值(掩码框)、改作用域(两个降级按钮各点一次,核对其后果不同)、解绑(凭据库不动)、真删(两次点击)。断言点是上面「需真人确认」列出的那些:不可用的动作不出现;改值后活体 shell 里真的是新值;`scope → persistent` 真的写库;真删后该行从库侧分区消失、会话侧作用域如实变「仅本次会话」、历史多一条 `deleted`、其它三条外来记录一字未动;子代理调 `list` 不挂起、调四个写动作得到结构化 `DELEGATED_CALLER`(无对话框)。
421
- 3. **浏览器侧(0.4.2,只能真人做)**:在真实浏览器里打开四个密钥输入面,逐条核「需真人确认」里那 5 项——是否还弹自动填充建议、Chromium 是否还给"生成强密码"建议、是否还弹"保存密码?"、已装的密码管理器扩展是否仍捕获、以及**聚焦之后能否正常键入**(只读守卫的代价)。
456
+ 3. **浏览器侧(0.4.2 起,只能真人做)**:在真实浏览器里打开四个密钥输入面**与两个标识符输入(凭据键、标题)**,逐条核「需真人确认」里那几项——是否还弹自动填充建议(标识符字段尤其要看历史值下拉是否消失)、Chromium 是否还给"生成强密码"建议、是否还弹"保存密码?"、已装的密码管理器扩展是否仍捕获、以及**聚焦之后能否正常键入**(只读守卫的代价)。
422
457
  4. **旧版对照**:`git stash` 或 checkout `v0.3.0` 后 `git diff v0.3.0 -- src test` 可看到本轮的全部改动面;`.credentials.yaml` 的基线(大小与 SHA256)应在复测前后逐字节不变。
423
458
 
424
459
  ## 存储与传播
@@ -455,6 +490,11 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
455
490
 
456
491
  ### 已知限制(如实记录)
457
492
 
493
+ - **粘贴动作(R2)的覆盖边界(如实)**:composer 侧只能把文本 `insertText` 到光标处,**没有受控 value 写入能力**;**第三方插件自建的输入框不在覆盖范围内**——我们只能在官方座位放自己的按钮。我方那 7 个可编辑字段则是「与 `onChange` 同路」的受控写入(走同一个 setter)。
494
+ - **粘贴自动转胶囊(R3)只在值输入框、且只有真登记**:只覆盖胶囊的「密钥内容」;改值面 / 管理确认卡 / 索要卡片**都不做**自动登记,`@DSH_SECRET_*` 等引用文本被分类器第一步排除(引用是名字,不是材质)。
495
+ - **`selectionchange` 实时文案是契约外手段(R5 的 C)——需真人确认**:公开面只能**按需**读取选区(pull 式),**选区变化没有通知**;因此那条按钮的实时文案依赖对 DOM `selectionchange` 的监听。**失效模式**:① 非浏览器环境(没有 `document`)**不注册**,行为退回「按下时切换文案」;② 回调抛错被吞掉,同样退回;③ 没有可编辑元素聚焦时回答「无信息」(不猜);④ 选区变化不生成本插件的事件,文案可能**滞后一个事件循环**。**关键(不得当成已验证)**:注册这一层在本套件里**未经执行验证**(测试用的 React stub 把 `useEffect` 置为 no-op,用例只断言到 props 层),**也从未在活体 DOM 上验证过** ⇒ **需真人确认**;并且**任何行为都不依赖 C**:C 只影响文案的实时性,不影响按下时刻的判定、登记与退回。
496
+ - **测试缝的完整新增键(全部 test-only,无生产路径读取)**:`__cordisSecretAttach` 在 0.5.0 新增 `ATTACH_CSS`、8 个 privacy 键(`PRIVACY_RULES` / `PRIVACY_EXCLUSIONS` / `PRIVACY_THRESHOLDS` / `classifyPastedText` / `entropyBitsPerChar` / `distinctCharCount` / `isReferenceText` / `hasCjk`)、`ATTACH_EN`、`selectedSpan`、`selectedTextIn`、`liveSelectionCue`、`DEFAULT_ATTACH_SCOPE`;全部是常量或纯函数,**无状态、不含任何值**。
497
+ - **0.5.0 起需真人确认的项(一律不得写成已验证)**:① 浏览器里**是否真的不再弹自动填充建议 / 密码管理器是否仍捕获**(0.4.2 / 0.4.3 的抑制属性只能机械断言,现场行为仍要人看);② **剪贴板读取的授权提示**(`navigator.clipboard.readText()` 是否被拒、提示是否可理解);③ **R5 的实时文案**(契约外 `selectionchange`,见上一条);④ composer「粘贴」插入后的**编辑器焦点与光标位置**观感。
458
498
  - **转录气泡里的胶囊由 shipped 代码渲染,插件改不了那次点击的目标**:`@DSH_SECRET_*` 在用户消息气泡里被 `dsh-client-ui-primitives` 的 `projectUserText` 渲染成 `data-ref-chip="file"`,点击走它硬编码的 `openFile('DSH_SECRET_OPENAI')`。**0.3.0 起**:该 `openFile` 解析出的地址(`dsh-resource://file/session/<id>/DSH_SECRET_X`)已被本插件的右栏查看器接管,因此点击打开的是**我们的详情页**;0.2.x 的行为(打开一个同名文件)作为历史如实保留,且**接管随时可能被同档更长的 pattern 抢走**(见「0.3.0 的四项增强 → 原胶囊的接管是长度竞争」)。**编辑器内(草稿)的胶囊不受影响**:那里是插件自己注册的 `secret` 引用源,点击打开只读详情胶囊。同一条 shipped 规则也适用于 `agent/pre-step` 追加的那行注记(它正文里同样含 `@DSH_SECRET_*`,因此也会显示为 file chip)。逐行证据与"为什么不存在既是 chip 又不可点的 `@` 形态"见「人类主动附加密钥(反方向)→ 已知限制」。
459
499
  - **工具结果里的 `@DSH_SECRET_*` 没有注记解释**(值无关,不是泄漏):注记只为**本步引入的、载有标记的用户消息**追加,因此同一形状的标记出现在工具结果里时原样进入模型上下文且无注记覆盖,模型可能按"`@` = 文件路径"去解读它(会失败)。它不会因此获得任何值,也不影响绑定、授权或 shell 注入。详见「人类主动附加密钥(反方向)→ 已知限制」。
460
500
  - **`ctx.authorization` 只承载 `persistent`**:该 seam 的契约要求"本次尝试期间提交并观察到一条凭据记录"(否则 `NOT_COMMITTED`),而 `session` 授权按定义不得落盘。因此 `session` 请求走同一套对话框、但不进该 seam;`persistent` 请求完整走 `registerFlow` + `begin`。这是 seam 契约决定的取舍,不是省事。
@@ -468,8 +508,9 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
468
508
  - **R5(质量门 r2 报出,0.4.0 已知限制:重叠场景会低估可用性)**:列表的既定边界是**一个变量只占一行**;当同一变量**既有一条人工附加记录(`staged` / `bound`)又有一份经 `secret_request` 的活跃会话授权**时(复现两步:先把某变量附上但不发送 → 再对同名变量 `secret_request` 并批准),附加行先入行、`requestViews` 随即跳过该变量,于是列表只回 `{state:'staged', origin:'attach', source:'session'}` 这一行——**这枚变量其实已被授权、也可注入**,而问方向的授权只在本会话**历史**(`authorized` / 来源 `request`)里可见。影响范围**仅**是人类管理列表在重叠场景下**低估可用性、方向不完整**:它**不泄漏任何值**、**不误报可执行动作**(该行的 `can.*` 仍为真,`unbind` 仍能撤掉本会话的暴露),**安全不变量与权限边界未受影响**(口径由 `manageList` 的 `seen` 去重决定,发布后如需让一行同时承载两个方向,单独立项再改,0.4.0 不返工)。
469
509
  - **记录键可能撞名**:记录键由 name 推导,`recordKeyId` 把 `_` 换成 `-`,所以 `a_b` 与 `a-b` 指向同一条记录键;真删按记录键寻址,撞键时删掉的是同一条。
470
510
  - **历史里 `deleted` 行的 `scope` 字段记的是「被删掉那条的作用域」**(`persistent`),不是"现在还剩什么";会话里那份的当前作用域以管理列表为准(`session`)。
511
+ - **0.4.3 修的两个标识符字段:机械断言已就位,浏览器侧仍需真人确认**(如实标注):凭据键与标题自 0.4.3 起改用 `autocomplete="off"`(Chrome 团队文档给出的"值每次都不同、不可复用"字段的取值)、去掉稳定 `id`/`name`、改成隐式 label 关联 + 同文本 `aria-label`,并同样带只读直到聚焦的守卫;机械断言钉住这一整套(正对照证明修复前形态下必然失败)。**未经真人浏览器验证**的部分:① Chrome/Edge/Firefox/Safari 在这两个字段上是否**真的不再**给历史值/自动填充下拉(`autocomplete="off"` 与"去掉 `id`"都是**提示级**手段,MDN 明写"该属性是给浏览器的提示;有些浏览器可能不遵守");② 已装的 1Password / LastPass / Bitwarden(opt-in)/ Dashlane / Proton Pass 是否确实不再捕获这两个字段;③ 隐式 label 关联在现场的辅助技术(屏幕阅读器、语音控制)下是否与原先的显式 `for`/`id` 等效(MDN 提醒并非所有辅助技术都实现隐式关联;`aria-label` 是为此准备的显式可访问名)。以上 3 项在无浏览器控制与未安装这些扩展时均为**需真人确认**,本 README 不把它们写成已验证。**依据归属(勿混读)**:有文档支撑的是**浏览器键控行为**(web.dev 逐字:浏览器按 `name`,有些还看 `id`,对一次性/不可复用值推荐 `autocomplete="off"`)与**五个厂商忽略标记**(1Password 官方;Bitwarden / Dashlane / Proton 自家源码;LastPass 支持页级);而**"去掉稳定 `id`"是本插件自己的设计选择,不是厂商文档的推荐做法**——1Password 官方文档的原意相反,它建议给字段加唯一 `id`/`name` 以便识别。
471
512
  - **密钥输入控件不会被自动填充/被密码管理器捕获,但"现场是否还弹提示"只能真人确认**(0.4.2 修复,如实标注):代码层面已加齐抑制属性与只读守卫(见「安全不变量」第 7 条)并由两个 Client 测试机械钉住,**未经真人浏览器验证**的部分是:① Chrome/Edge/Firefox/Safari 在四个密钥字段上是否**完全不再**给自动填充建议;② Chromium 在 `autocomplete="new-password"` 字段上可能出现的"生成强密码"建议是否出现、是否可忽略(它不会回填已保存的凭据,也不读取字段);③ 是否还会弹"保存密码?"气泡;④ 1Password / LastPass / Bitwarden / Dashlane / Proton Pass 扩展在现场是否确实**不再**捕获或提供保存(各厂商忽略标记的实际效力只能在其真实扩展上验;其中 Bitwarden 的忽略还是 **opt-in**,LastPass 那条只有支持页标题级引用);⑤ **只读直到聚焦的守卫在现场是否有代价**——辅助技术可能把 `readOnly` 字段读成"只读"、iOS 上 `readOnly` 可能压住软键盘(需要二次点按)、`disabled` 的字段永不释放、以及守卫目前只在 props 层被断言(真实 DOM 重渲染下是否稳定释放未验),因此"**聚焦之后能正常键入**"必须由真人确认。以上 5 项在无浏览器控制与未安装这些扩展时均为**需真人确认**,本 README 不把它们写成已验证。
472
- - **只读直到聚焦的守卫自身有代价(0.4.2 新增)**:四个密钥字段默认以 `readOnly` 渲染,聚焦时才由 `onFocus` 释放。已知代价:① 辅助技术(屏幕阅读器)可能把它读成只读字段;② iOS Safari 等平台上 `readOnly` 可能阻止软键盘自动弹出,需要用户二次点按;③ 若字段处于 `disabled` 状态则永不释放(本插件在不应输入的场合才这么做);④ 该守卫目前只有 props 层断言(正对照证明修复前缺失),**真实 DOM 重渲染下是否每次都释放未验证**——属需真人确认项(见上一条 ⑤)。这是为防自动填充付出的可用性代价,如实记录,不粉饰。
513
+ - **只读直到聚焦的守卫自身有代价(0.4.2 新增;0.4.3 起也覆盖两个标识符字段)**:四个密钥字段与两个标识符字段默认以 `readOnly` 渲染,聚焦时才由 `onFocus` 释放。已知代价:① 辅助技术(屏幕阅读器)可能把它读成只读字段;② iOS Safari 等平台上 `readOnly` 可能阻止软键盘自动弹出,需要用户二次点按;③ 若字段处于 `disabled` 状态则永不释放(本插件在不应输入的场合才这么做);④ 该守卫目前只有 props 层断言(正对照证明修复前缺失),**真实 DOM 重渲染下是否每次都释放未验证**——属需真人确认项(见上一条 ⑤)。这是为防自动填充付出的可用性代价,如实记录,不粉饰。
473
514
  - **0.3.0 新增的"需真人确认"清单(如实标注,未验证即写"未验证")**:① **旁挂胶囊确实渲染在用户气泡之后**(含四档"工作步骤展示"下的实际位置与视觉贴合);② **点旁挂胶囊**打开的是输入框上方的信息框;③ **点原胶囊**打开的是右侧栏详情页(且**不再是**"文件不存在"),以及页面顶栏右栏行为符合预期;④ **原胶囊接管的可争用性**在现场的表现(装/卸 `dsh-better-sidebar`,或临时注册一个更长的同档 pattern,观察是否被静默抢走);⑤ **移除即撤销**的真实时序(删掉 → 约 0.6s 后 `GET /api/secret.attached` 不再是 `staged`;删掉后 600ms 内插回 ⇒ 记录仍在;**发送不得触发撤销**);⑥ **历史区**在刷新后仍在、宿主重启/插件重载后清空、重放/分叉会话不重建;⑦ **`@` 菜单**两类来源的 `section`/`description` 文案在实际宽度下不被截断,且凭据库条目的确认→登记→插入链路真的能取到值。以上 7 项在无浏览器控制时均为**需真人确认**,本 README 不把它们写成已验证。
474
515
 
475
516
  ## 开发
@@ -520,6 +561,12 @@ npm test # node --test 八个文件:
520
561
  npm run build # 产出 lib/(Host 半 + 浏览器产物 ./client)
521
562
  ```
522
563
 
564
+ **0.5.0 给测试面加的东西(如实记录)**:
565
+
566
+ - **新用例面**:`test/unit.test.ts` 增加 R1 的命名纯函数(形态 token 化 / 提示词不含明文 / 本地兜底键与去重 / 模型答案清洗 / 模型挂起不阻塞,deadline 内必答)与 R3 的分类器(规则、排除项、阈值,命中与不命中两侧,含 31/32 这类边界);`test/register.test.ts` 增加 R1 的接线(无 llm 时激活且 attach 成功=正对照、模型键被采用且**不带 sessionId / 不含明文 / 不 append 事件**、答案不可用则回落、无 route 时不调模型);`test/client-attach.test.ts` / `test/client-card.test.ts` 增加 R1 空键与非法键、R2 的七处「粘贴」动作与 composer 座位、R3 的两条粘贴路径与三条失败分支、R4 的 ✕ 解绑与显隐、R5 的选区转换与退回。
567
+ - **测试基础设施的一处修正(如实记录)**:渲染器从「所有组件共用一份 state 数组」改成**每个组件各自独立的 state**(对齐真实 React 语义;此前共用一个数组会让不同组件的 `useState` 槽互相污染)。
568
+ - **测试 fetch 助手的另一处修正(如实记录)**:从「按前缀匹配」改成**按 `pathname` 精确路由**——前缀匹配会**把 `/api/secret.attached` 误配到 `/api/secret.attach`**(前者以前缀包含后者),这个历史教训留在用例注释里。
569
+
523
570
  ## 设计要点
524
571
 
525
572
  - **附加方向的两段式生命周期**:值先在客户端本地 state,再经一次 POST 进 Host 内存的暂存表;只有携带标记的用户消息成为 durable 事件时才提升为 grant,`anchorSeq` 取该事件的 seq。这样"未发送就永不注入",且锚点不依赖任何"回头扫日志"的启发式。
@@ -575,7 +622,7 @@ npm stage approve <stage-id> # 需要 2FA
575
622
  判断该版本是否已存在于目标 registry,已存在就跳过发布(并在 Step Summary 写明"该版本已存在,跳过发布"),
576
623
  因此对已发布版本重推 tag 不会产生必然失败的公开红叉。其它检查错误(网络、鉴权、registry 故障)仍会让 job 失败。
577
624
 
578
- **registry 现状(截至本文)**:npmjs 上已上线 `0.0.0-stage`、`0.1.0`、`0.2.0`、`0.2.1`、`0.3.0`(`dist-tags.latest = 0.3.0`)。`0.4.0` 与 `0.4.1` 都已由 CI 放入 stage 队列、**都还没被批准**:`0.4.0` 的 stage id `dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`(shasum `22dfadfc613695018adfd5ee09eb8b71b1ba9edc`)**不建议批准**(该制品有启动缺陷,见「0.4.0 的发布级缺陷与 0.4.1 修复」),`0.4.1` 的 stage id `22abf7a3-606b-4ee4-a1f9-5e84b557d6d0`(shasum `9b64a832abad45123ec96b3c2eec398c4b7377dd`,provenance 已发布到 Sigstore 透明度日志 `logIndex=3142251804`)才是要批准的那个:`npm stage approve 22abf7a3-606b-4ee4-a1f9-5e84b557d6d0`。已经躺在队列里的 0.4.0 可以显式丢弃:`npm stage reject dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`(本机 npm CLI 12.2.0 的 `npm stage` 确实有 `reject` 子命令,帮助文本:「Reject a staged package, removing it from the registry」,可用 `--otp <otp>`)——**批准与丢弃都需要登录 + 2FA,只能由人来做**;本发布流程不登录 npm、不读取或处理任何用户令牌、不代做任何需要 2FA 的动作。GitHub Packages 上 `0.1.0`/`0.2.0`/`0.2.1`/`0.3.0`/`0.4.1` 都在(`0.4.1` 的包版本 id `1354170442`,上一个可用版本 `0.3.0` 的是 `1346087280`),另有**不可用**的 `0.4.0`(包版本 id `1351737947`)——GitHub Packages 由 CI 直接发布、不经人工批准,所以撤不回,只能靠 Release notes 的警告与本文说明。
625
+ **registry 现状(截至本文)**:npmjs 上已上线 `0.0.0-stage`、`0.1.0`、`0.2.0`、`0.2.1`、`0.3.0`、`0.4.1`、`0.4.2`(`dist-tags.latest = 0.4.2`,2026-10-08T12:09:17Z 上线)。三个 0.4.x 的 stage 队列历史与**现状**逐个写清,避免批错版本:`0.4.0` 的 stage id `dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`(shasum `22dfadfc613695018adfd5ee09eb8b71b1ba9edc`)**从未被批准、从未上线**(该制品有启动缺陷,见「0.4.0 的发布级缺陷与 0.4.1 修复」),建议显式丢弃:`npm stage reject dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`;`0.4.1` 的 stage id `22abf7a3-606b-4ee4-a1f9-5e84b557d6d0`(shasum `9b64a832abad45123ec96b3c2eec398c4b7377dd`,provenance `logIndex=3142251804`)**已由用户 2FA 批准上线**(2026-10-08T09:30:00Z),现已被 0.4.2 取代;`0.4.2` 的 stage id `a4056fb7-0a1a-4740-91d3-856665498b5e`(shasum `60e61cc5395e2f279a8b9a0a769165ecf4b3c403`、integrity `sha512-S34f977fgdNiR…HDV7DobbX5K8A==`、provenance `logIndex=3146400254`;由 tag `v0.4.2` 推送触发:Release run `37764853236` / Publish run `37764853223`,tag 对象 `b5a20422edde9b3951ba433945c72c9a430c3e0f`(GitHub 显示 verified),Release <https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.2>)**已由用户 2FA 批准上线**(2026-10-08T12:09:17Z),即当前的 `dist-tags.latest`;`0.4.3` 的 stage id `9cc6b07d-c766-480c-aaf7-ae27bf92a909`(shasum `ad704fadf5a40a1f25f75a1f335fb4492db3f96d`、integrity `sha512-Y0uUC18gdQvCo…lsva4nx5iVumg==`、provenance `logIndex=3150146502`;由 tag `v0.4.3` 推送触发:Release run `37798652085` / Publish run `37798652062`,tag 对象 `ac5e96deca20b506852c98b96820a06990275478`、GitHub 显示 verified,Release <https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.3>)**已放入队列、尚未批准**——**此刻队列里唯一的待批准版本就是 `0.4.3`**(批准用 `npm stage approve 9cc6b07d-c766-480c-aaf7-ae27bf92a909`;已上线的版本不能再 `npm stage reject`)。本机 npm CLI 12.2.0 的 `npm stage` 确实有 `reject` 子命令,帮助文本:「Reject a staged package, removing it from the registry」,可用 `--otp <otp>`——**批准与丢弃都需要登录 + 2FA,只能由人来做**;本发布流程不登录 npm、不读取或处理任何用户令牌、不代做任何需要 2FA 的动作。GitHub Packages 上 `0.1.0`/`0.2.0`/`0.2.1`/`0.3.0`/`0.4.1`/`0.4.2`/`0.4.3` 都在(`0.4.3` 的包版本 id `1356811507`;`0.4.2` 的是 `1355352850`;`0.4.1` 的是 `1354170442`;上一个可用版本 `0.3.0` 的是 `1346087280`),另有**不可用**的 `0.4.0`(包版本 id `1351737947`)——GitHub Packages 由 CI 直接发布、不经人工批准,所以撤不回,只能靠 Release notes 的警告与本文说明。
579
626
 
580
627
  同一份包也会发布到 **GitHub Packages**(`npm.pkg.github.com`,`github-packages` job,用内置 `GITHUB_TOKEN`),
581
628
  使包在仓库页面上可见、可被 `@xinvxueyuan:registry=https://npm.pkg.github.com` 的消费者安装。
@@ -605,7 +652,13 @@ npm stage approve <stage-id> # 需要 2FA
605
652
 
606
653
  ### GitHub Release 与签名
607
654
 
608
- > **已发生的事实**:`v0.4.1` 的 Release 是 https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.1
655
+ > **已发生的事实**:`v0.4.3` 的 Release 是 https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.3
656
+ > (2026-10-08 发布,`draft: false`;`release.yml` 运行 `37798652085` 成功,`publish.yml` 运行 `37798652062` 成功),附件三件:`xinvxueyuan-cordis-plugin-secret-0.4.3.tgz`(275823 B,sha256 `a9f46d191c19923370999d99826362c8585ce13d996fa628fa2d47c51d92a895`,与 `SHA256SUMS` 里记的一致,也与本机 `core.autocrlf=false` 干净克隆里 `npm pack` 的产物逐字节一致)、`SHA256SUMS`(109 B,sha256 `101ae23dfe5a518ac7fd90d8d31081763c885833d29fe90bf2c925394f0c8c00`)、`SHA256SUMS.asc`(887 B,sha256 `4db2af4f46d7a11d49692a198136a9d97c4043fac1d9de4674d61d4ceab745ba`;RSA 4096 `6C6FD9B2…72B7B35A` 对 **CI 那份 `SHA256SUMS`** 的分离签名,本机 `gpg --verify` 通过);
657
+ > tgz 与 SHA256SUMS 都由 `gh attestation verify` 可验(一份 attestation,subject 同时列出两者),且**证明绑定在 tag 上**(builder id / 签名证书 SAN = `.../release.yml@refs/tags/v0.4.3`,`externalParameters.workflow.ref` = `refs/tags/v0.4.3`,`resolvedDependencies` = `git+https://github.com/xinvxueyuan/cordis-plugin-secret@refs/tags/v0.4.3` @ commit `05f0b142af5b2c0aff0c6668b43d985ad5270e61`)——即 tag→commit 是证明的一部分,而不是只绑定到 `refs/heads/main`(`--format json` 全文对 `refs/heads/` **零命中**)。tag 对象为 annotated + GPG 签名
658
+ > (`git cat-file -t v0.4.3` → `tag`,tag 对象 sha `ac5e96deca20b506852c98b96820a06990275478`;GitHub API 的 `verification.verified` → `true`,`reason` → `valid`,`verified_at` → `2026-10-08T15:12:27Z`)。
659
+ > 上一个版本 `v0.4.2` 的 Release 是 https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.2
660
+ > (2026-10-08 发布,`draft: false`;同形三附件),`xinvxueyuan-cordis-plugin-secret-0.4.2.tgz` 271615 B、sha256 `90574cef477c64b152a6d7062e920f62dce01892e185e3d8bb8f11d4d40a51b9`(与 `SHA256SUMS` 一致),tag 对象 `b5a20422edde9b3951ba433945c72c9a430c3e0f`(GitHub 显示 verified);该版本**已由用户 2FA 批准上线**(`dist-tags.latest`)。
661
+ > 再上一个版本 `v0.4.1` 的 Release 是 https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.1
609
662
  > (2026-10-08 发布,`draft: false`;`release.yml` 运行 `37735146290` 成功,`publish.yml` 运行 `37735146256` 成功),附件三件:`xinvxueyuan-cordis-plugin-secret-0.4.1.tgz`(262988 B,sha256 `46286a2eefebb0809696377ebfecf7569c8aa723287cbe7a8e2c7bee979a76f8`,与 `SHA256SUMS` 里记的一致,也与本机 `core.autocrlf=false` 干净克隆里 `npm pack` 的产物逐字节一致)、`SHA256SUMS`(109 B,sha256 `ddd3b78232803958fefd699bf7488d208db97462a8985eef891a766598f643dd`)、`SHA256SUMS.asc`(887 B,sha256 `3ded718895f81d5b03b8b6a1037f2b1138d87d10dd9f58523b88a67bc3934ec1`;RSA 4096 `6C6FD9B2…72B7B35A` 对 **CI 那份 `SHA256SUMS`** 的分离签名,本机 `gpg --verify` 通过);
610
663
  > tgz 与 SHA256SUMS 都由 `gh attestation verify` 可验(一份 attestation,subject 同时列出两者),且**证明绑定在 tag 上**(builder id / 签名证书 SAN = `.../release.yml@refs/tags/v0.4.1`,`externalParameters.workflow.ref` = `refs/tags/v0.4.1`,`resolvedDependencies` = `git+https://github.com/xinvxueyuan/cordis-plugin-secret@refs/tags/v0.4.1` @ commit `744f5a675d3641cbf6a7dd0121e88b5fa333f4bf`)——即 tag→commit 是证明的一部分,而不是只绑定到 `refs/heads/main`(`--format json` 全文对 `refs/heads/` **零命中**)。tag 对象为 annotated + GPG 签名
611
664
  > (`git cat-file -t v0.4.1` → `tag`,tag 对象 sha `43da6f8a85797be40356691db38079de50fa0844`;GitHub API 的 `verification.verified` → `true`,`reason` → `valid`,`verified_at` → `2026-10-08T06:00:40Z`)。
@@ -630,25 +683,25 @@ npm stage approve <stage-id> # 需要 2FA
630
683
  | 构建来源证明 | `release.yml` 调用 `actions/attest-build-provenance`(pin 到 commit SHA),为 **tgz 与 SHA256SUMS 两者**生成 Sigstore 签名的 SLSA 构建来源证明,可用 `gh attestation verify` 校验。 |
631
684
  | npm 侧 | `release.yml` **完全不执行任何 npm publish**;npm 发布只由上面的 `publish.yml` staged publishing 负责。 |
632
685
 
633
- 维护者操作顺序(`v0.2.0`、`v0.2.1`、`v0.3.0`、`v0.4.0` 与 `v0.4.1` 都已按此执行):
686
+ 维护者操作顺序(`v0.2.0`、`v0.2.1`、`v0.3.0`、`v0.4.0`、`v0.4.1`、`v0.4.2` 与 `v0.4.3` 都已按此执行):
634
687
 
635
688
  ```sh
636
689
  # 1) 本机确认工作区干净、package.json 的 version 已就位(版本号由发布者手工提升)
637
690
  git status --porcelain
638
691
 
639
692
  # 2) 创建 annotated + GPG 签名 tag(私钥仅在本机使用;本机需能完成 GPG 签名)
640
- git tag -s v0.4.1 -m "v0.4.1"
693
+ git tag -s v0.4.3 -m "v0.4.3"
641
694
 
642
695
  # 3) 只推 tag —— release.yml 会构建产物、生成来源证明并创建 Release(不要用 workflow_dispatch:那样证明会绑到 refs/heads/main)
643
- git push origin v0.4.1
696
+ git push origin v0.4.3
644
697
 
645
698
  # 4) 取 Release 上 CI 生成的那份 SHA256SUMS,做分离签名并附回 Release(私钥不进 CI)
646
- gh release download v0.4.1 --pattern SHA256SUMS --clobber
699
+ gh release download v0.4.3 --pattern SHA256SUMS --clobber
647
700
  gpg --armor --detach-sign SHA256SUMS # 生成 SHA256SUMS.asc
648
- gh release upload v0.4.1 SHA256SUMS.asc --clobber
701
+ gh release upload v0.4.3 SHA256SUMS.asc --clobber
649
702
  ```
650
703
 
651
- > **为什么第 4 步要下 CI 的 `SHA256SUMS` 而不是签本机 `npm pack` 那份**:本机是 Windows 且 `core.autocrlf=true`,工作树里 `LICENSE-MIT`/`LICENSE-APACHE` 被 checkout 成 CRLF,本机打出的 tgz 与 CI(Linux checkout,LF)打出的只差这两个文件的行尾——`0.4.0`:本机 260311 B / `0a6fd66c…` 对 CI 260287 B / `4612be9c…`;`0.4.1`:本机 263015 B / `4b3c3a7d…` 对 CI 262988 B / `46286a2e…`。在 `core.autocrlf=false` 的干净克隆里本机 `npm pack` 复现出的正是 CI 的那份(`docs/` 之类不进包,`lib/**` 与 `src/**` 逐字节相同)。签错那份会让 `gpg --verify SHA256SUMS.asc SHA256SUMS` 在下载来的附件上对不上。
704
+ > **为什么第 4 步要下 CI 的 `SHA256SUMS` 而不是签本机 `npm pack` 那份**:本机是 Windows 且 `core.autocrlf=true`,工作树里 `LICENSE-MIT`/`LICENSE-APACHE` 被 checkout 成 CRLF,本机打出的 tgz 与 CI(Linux checkout,LF)打出的只差这两个文件的行尾——`0.4.0`:本机 260311 B / `0a6fd66c…` 对 CI 260287 B / `4612be9c…`;`0.4.1`:本机 263015 B / `4b3c3a7d…` 对 CI 262988 B / `46286a2e…`;`0.4.3`:本机 275850 B / `565707f0…` 对 CI 275823 B / `a9f46d19…`。在 `core.autocrlf=false` 的干净克隆里本机 `npm pack` 复现出的正是 CI 的那份(`docs/` 之类不进包,`lib/**` 与 `src/**` 逐字节相同)。签错那份会让 `gpg --verify SHA256SUMS.asc SHA256SUMS` 在下载来的附件上对不上。
652
705
 
653
706
  校验方式:
654
707
 
@@ -660,9 +713,9 @@ sha256sum -c SHA256SUMS
660
713
  gpg --verify SHA256SUMS.asc SHA256SUMS
661
714
 
662
715
  # 校验构建来源证明(需要 gh CLI)
663
- gh attestation verify xinvxueyuan-cordis-plugin-secret-0.4.1.tgz --repo xinvxueyuan/cordis-plugin-secret
716
+ gh attestation verify xinvxueyuan-cordis-plugin-secret-0.4.3.tgz --repo xinvxueyuan/cordis-plugin-secret
664
717
  gh attestation verify SHA256SUMS --repo xinvxueyuan/cordis-plugin-secret
665
- gh attestation verify xinvxueyuan-cordis-plugin-secret-0.4.1.tgz --repo xinvxueyuan/cordis-plugin-secret --format json # 查 externalParameters.workflow.ref 是否为 refs/tags/v0.4.1
718
+ gh attestation verify xinvxueyuan-cordis-plugin-secret-0.4.3.tgz --repo xinvxueyuan/cordis-plugin-secret --format json # 查 externalParameters.workflow.ref 是否为 refs/tags/v0.4.3
666
719
  ```
667
720
 
668
721
  补充说明: