@banbolee/dsh-fish-shell 0.7.0 → 0.8.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +318 -13
  3. package/README.zh.md +101 -4
  4. package/THIRD-PARTY-NOTICES.md +91 -0
  5. package/cordis.patch.yml +56 -0
  6. package/node_modules/@deepseek-ai/cosmokit/LICENSE +21 -0
  7. package/node_modules/@deepseek-ai/cosmokit/README.md +30 -0
  8. package/node_modules/@deepseek-ai/cosmokit/lib/index.js +418 -0
  9. package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts +15 -0
  10. package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts.map +1 -0
  11. package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts +13 -0
  12. package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts.map +1 -0
  13. package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts +41 -0
  14. package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts.map +1 -0
  15. package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts +89 -0
  16. package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts.map +1 -0
  17. package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts +19 -0
  18. package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts.map +1 -0
  19. package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts +42 -0
  20. package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts.map +1 -0
  21. package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts +40 -0
  22. package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts.map +1 -0
  23. package/node_modules/@deepseek-ai/cosmokit/package.json +32 -0
  24. package/node_modules/@deepseek-ai/cosmokit/src/array.ts +42 -0
  25. package/node_modules/@deepseek-ai/cosmokit/src/index.ts +12 -0
  26. package/node_modules/@deepseek-ai/cosmokit/src/misc.ts +78 -0
  27. package/node_modules/@deepseek-ai/cosmokit/src/string.ts +113 -0
  28. package/node_modules/@deepseek-ai/cosmokit/src/time.ts +92 -0
  29. package/node_modules/@deepseek-ai/cosmokit/src/types.ts +168 -0
  30. package/node_modules/@deepseek-ai/cosmokit/src/volatile.ts +86 -0
  31. package/node_modules/@deepseek-ai/dsh-tool-terminal/LICENSE +21 -0
  32. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.i18n.yaml +100 -0
  33. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.md +186 -0
  34. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.zh.md +186 -0
  35. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/index.js +646 -0
  36. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/background.d.ts +20 -0
  37. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/index.d.ts +32 -0
  38. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/render.d.ts +81 -0
  39. package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json +63 -0
  40. package/node_modules/@deepseek-ai/schemastery/LICENSE +21 -0
  41. package/node_modules/@deepseek-ai/schemastery/README.md +393 -0
  42. package/node_modules/@deepseek-ai/schemastery/lib/index.cjs +635 -0
  43. package/node_modules/@deepseek-ai/schemastery/lib/index.mjs +635 -0
  44. package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts +211 -0
  45. package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts.map +1 -0
  46. package/node_modules/@deepseek-ai/schemastery/package.json +39 -0
  47. package/node_modules/@deepseek-ai/schemastery/src/index.ts +962 -0
  48. package/node_modules/@standard-schema/spec/LICENSE +21 -0
  49. package/node_modules/@standard-schema/spec/README.md +198 -0
  50. package/node_modules/@standard-schema/spec/dist/index.cjs +18 -0
  51. package/node_modules/@standard-schema/spec/dist/index.d.cts +119 -0
  52. package/node_modules/@standard-schema/spec/dist/index.d.ts +119 -0
  53. package/node_modules/@standard-schema/spec/dist/index.js +0 -0
  54. package/node_modules/@standard-schema/spec/package.json +52 -0
  55. package/package.json +56 -42
  56. package/terminal-tools.js +259 -0
  57. package/terminal.js +297 -0
  58. package/tool.js +1 -1
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](./README.md) | **中文**
4
4
 
5
- DeepSeek Harness 的 fish shell executor 与工具:用 **fish** 而不是 bash 运行命令。可直接分发、与 surface 无关:任何挂载它的 profile 都能用——基于 preset roster 的(dsh-tui、web)或基于 host tool 的(headless)——并且在**任意 agent preset**(standard、ptc、cordis、minimal 或第三方 preset)下都能用。
5
+ DeepSeek Harness 的 fish shell executor 与工具:用 **fish** 而不是 bash 运行命令。bundle 在设计上与 surface 无关——基于 preset roster 的(dsh-tui、web)或基于 host tool 的(headless)——per-agent policy 也按「任意 agent preset 都适用」编写(standard、ptc、cordis、minimal 或第三方 preset)。但**已验证范围比设计更窄**:真实 lane 覆盖的是 `dsh-base` 类 profile(隔离 profile 的 bundle layer 为 `@deepseek-ai/dsh-base` + 本 bundle)、`dsh-tui` 组合与官方 `minimal` preset。其它 preset/profile 组合一律**未验证**,部署到别处之前请以下方 **已知限制** 为权威口径。
6
6
 
7
7
  ## 这个 bundle 做什么
8
8
 
@@ -18,6 +18,30 @@ DeepSeek Harness 的 fish shell executor 与工具:用 **fish** 而不是 bash
18
18
  - **Per-agent fish policy**(`policy.js`,导出为 `@banbolee/dsh-fish-shell/policy`):一个以 host 方式挂载的 Cordis plugin,让每个 agent 都看到 fish 而不是 bash,**无论它跑在哪个 agent preset 下**。bundle patch 会禁用基础 bash executor(`bash-sandbox`)与 host bash 工具(`tool-bash`),挂载 fish executor 与 host-global `fish` 工具,并且不改动 preset——它不再修改 roster 默认值、不再注入 preset root,也不再往 `$DSH_HOME/.agent-presets/` 下写副本。preset 仍会注册自己的 `bash` 同名工具(standard/minimal 就会),而在 `agent/created` 时(以及每次 `tools/change`,即 `/preset` 重新组合之后),policy 会通过 `agent.ctx.tools.restrict({ deny: ['bash'] })` 按 agent 隐藏这个继承来的 bash 工具,并用一个空的 agent 作用域小节遮蔽 preset 静态的 `tool:bash` 提示词指引。agent 最终只剩下唯一一个 shell 工具:`fish`。在 `minimal` 下——它常驻的 bash 是持久 PTY 形态(参数只带 `command`,输出 schema 是普通字符串)——policy 会先在 agent 作用域注册一个持久 `fish` 工具(遮蔽 one-shot fish),然后隐藏 bash,因此 minimal session 在 fish 下保持持久语义。
19
19
  - **持久 fish terminal 后端**(`terminal-fish.js`,导出为 `@banbolee/dsh-fish-shell/terminal-fish`):一个库——**不**由 bundle patch 挂载——提供持久工具使用的 `fish` PTY 后端。它继承官方 `BashTerminalBackend`,启动 `fish --no-config -i`(可通过配置覆盖)并配上受控提示符——该提示符遵循 harness readiness 契约(OSC `133;D;<status>` + `dsh> `)——并通过同一套 sandbox policy 施加限制;官方 sandbox-mode fence 被复制进来(官方模块没有导出它;这份副本为自管理用途提供了一个可注入的 owner-activity 检查)。patch 刻意没有 `fish-terminal` host row:`terminals` service 是 `minimal` preset isolate realm 的 entry-local 资源,在 host 平面(headless / dsh-tui / composition 测试 profile)不可见,因此这样一行会永远停留在 pending。`apply` 会被导出,供直接装配与测试使用(或供显式挂载该 registry 的 composition 使用)。
20
20
  - **持久 fish 工具**(`persistent.js`,导出为 `@banbolee/dsh-fish-shell/persistent`):官方 `dsh-tool-bash-persistent` 模式的 fish 版本——每个 agent 一个缓存的 PTY shell,其 cwd、变量与函数在多次调用之间存活,并支持 marker 包裹的命令、串行执行、deadline/timeout 重置与 scrollback 读取。它刻意不导出 Cordis `apply`:`policy.js` 会在 agent 边界为 persistent-bash preset 调用 `registerPersistentFish(ctx, agentCtx)`。PTY 是**自管理**的:工具自己持有一个 `FishTerminalBackend` 实例并直接驱动它的 session(`backend.spawn(...)`、`session.startSend/read/status/close`),而不经过 `terminals` registry——agent 作用域的工具解析不到它(该 registry 只存在于 minimal preset 的 entry-local realm 内)。命令的引号处理使用 fish 单引号转义,并用带单个字符串参数的 `eval`(fish 4 没有 `$'...'` ANSI-C 引号语法,也没有 `eval --`),已用真实 fish 4.0.0 验证。
21
+ - **交互式终端会话**(`terminal-tools.js`,导出为 `@banbolee/dsh-fish-shell/terminal-tools`):交互式(L2)surface 的 `terminalTools: 'allow' | 'deny'` 开关;它的 patch group 挂载官方 `@deepseek-ai/dsh-terminal` registry、本 bundle 的 `terminal.js`(用 fish argv 驱动的官方 `@deepseek-ai/dsh-terminal-bash` 后端,外加 PTY 子进程环境里的 harness home 契约),以及官方 `@deepseek-ai/dsh-tool-terminal`(六个 `terminal_*` 工具)。见下方 **交互式终端会话(L2)**。
22
+
23
+ ## 支持矩阵
24
+
25
+ | 项 | 支持范围 | 验证情况 |
26
+ |---|---|---|
27
+ | `@deepseek-ai/*` dsh 族 | `0.2.0-rc.2` —— 精确版本钉在仓库根 `package.json`(`pnpm.overrides`) | 是:S0 探针在真实隔离 profile 里跑过 |
28
+ | `fish` | `3.7+` | 是,实测 `3.7.1`;更低版本未验证 |
29
+ | 平台 | 仅 POSIX:Linux 与 macOS。Windows 不支持(Windows 由 `pwsh` 系列覆盖) | Linux:`test` job 里 `DSH_REAL_FISH_PTY=1` 的真实 lane;macOS:`fish-pty-macos` job 跑真实 L2 lane,含 `workspace-write` 下 workspace 外写入被拒 |
30
+ | Node.js | `>=22`(`engines.node`) | 是 |
31
+
32
+ ## 测试覆盖矩阵
33
+
34
+ 上面的承诺,只由真正断言它的 lane 兜底。下表列出每条 lane、它启动什么、覆盖什么:
35
+
36
+ | Lane | 启动 | 覆盖 |
37
+ |---|---|---|
38
+ | `plugins/fish-shell/tests/terminal-session-real.spec.ts` | 经 `dsh` CLI 启动的真实隔离 `dsh-base` profile | 六个工具;`open`/`send`/`read`/`close`;`terminal_signal`;后台 `terminal_send` job 与 `job_output`/`job_kill`;切 mode 的 fence;`workspace-write` 下 workspace 外写入被拒;`read-only` 语义;owner 隔离(`FOREIGN_SESSION`);harness home 契约(会话内 `$DSH_HOME` 就是本 profile 的 home,嵌套 `dsh --profile <profile> --dump-config` 组合出的正是该 profile,而同一个命令换到陌生 home 下会失败) |
39
+ | `tests/composition/fish-pty-dsh-tui.spec.ts` | 真实的 `dsh-tui` 组合 | bundle group 激活(没有 pending 的 L2 row);六个工具可见;`open`/`send`/`read`/`close` |
40
+ | `tests/composition/fish-pty-minimal.spec.ts` | `dsh-base` + 官方 `minimal` preset | 两个 `terminals` realm(host fish group + preset 的 `/bin/bash` 持久 shell);六个工具各恰好一次;持久 fish 替换;`open`/`send`/`read`/`close` |
41
+ | `tests/composition/fish-pty-bundled-row.spec.ts` | 直引 `@deepseek-ai/dsh-tool-terminal` 的 profile patch | 只从本 bundle 内部解析;该 row 停在 `pending (waiting for service: terminals)`;不会重复注册;存活的那份注册仍能驱动 PTY |
42
+ | `scripts/qa/fish-packed-offline.mjs` | 用 `--offline` 装进临时 profile 的已打包 tarball | 安装成功且真的加载:六个工具可见、`listBackends()` 非空 |
43
+
44
+ CI 里:真实 lane 由 `test` job(Linux)与 `fish-pty-macos`(macOS)跑,composition lane 由 `composition` job 跑,打包/离线 lane 由 `fish-packed-offline` job 跑。
21
45
 
22
46
  ## 安装
23
47
 
@@ -33,11 +57,50 @@ dsh plugin --profile <name> add @banbolee/dsh-fish-shell
33
57
  dsh plugin --profile <name> add ./plugins/fish-shell
34
58
  ```
35
59
 
36
- 把它加到每一个需要运行 fish 的 profile(dsh-tui、web、headless……)。bundle patch(`cordis.patch.yml`)是一个跨 surface 安全的 patch:它禁用 host bash executor 与 `tool-bash` row,挂载 fish executor 与 host-global `fish` 工具,并插入 `fish-preset-policy` row——该 row 会按 agent 隐藏 preset 继承来的 `bash` 工具(restriction + 空的 `tool:bash` 提示词遮蔽,在 `minimal` 下换成 persistent fish)。patch 不再改动 preset roster。
60
+ 把它加到每一个需要运行 fish 的 profile(dsh-tui、web、headless……)——但要记住上面的口径:`dsh-base` 类 profile、`dsh-tui` 组合与官方 `minimal` preset 已验证,其它 preset/profile 组合都未验证(见 **已知限制**)。bundle patch(`cordis.patch.yml`)是一个跨 surface 安全的 patch:它禁用 host bash executor 与 `tool-bash` row,挂载 fish executor 与 host-global `fish` 工具,并插入 `fish-preset-policy` row——该 row 会按 agent 隐藏 preset 继承来的 `bash` 工具(restriction + 空的 `tool:bash` 提示词遮蔽,在 `minimal` 下换成 persistent fish)。它还会为交互式终端会话追加 `fish-terminal-group`(见下方 **交互式终端会话(L2)**)。patch 不再改动 preset roster。
37
61
 
38
62
  ### 部署副本与 symlink 链
39
63
 
40
- 这些 executor 继承 `@deepseek-ai/dsh-bash-local` / `@deepseek-ai/dsh-bash-sandbox`。这些 import 必须解析到 harness 使用的同一个运行时实例——即 launcher 维护的 `profiles/node_modules/@deepseek-ai/*` symlink 链。直接 `link:` 到本仓库检出(位于 profile 树之外)将解析不到任何 `@deepseek-ai` 包,因此部署副本放在 `profiles/node_modules/@banbolee/dsh-fish-shell`(树内)。`scripts/sync-to-profile.sh` 会在改动后把插件复制到那里。pnpm 的 `file:` 依赖让每个 profile 指向该部署副本。发布到 npm 的包则正常安装(它的 realpath 本来就在 profile 树内)。
64
+ 这些 executor 继承 `@deepseek-ai/dsh-bash-local` / `@deepseek-ai/dsh-bash-sandbox`。这些 import 必须解析到 harness 使用的同一个运行时实例——即 launcher 维护的 `profiles/node_modules/@deepseek-ai/*` symlink 链。直接 `link:` 到本仓库检出(位于 profile 树之外)将解析不到任何 `@deepseek-ai` 包,因此部署副本放在 `profiles/node_modules/@banbolee/dsh-fish-shell`(树内)。`scripts/sync-to-profile.sh <profile>` 会用 hoisted linker 打包插件(先 `pnpm install --prod --config.node-linker=hoisted`,再 `pnpm pack --config.node-linker=hoisted`),并用 `dsh plugin --profile <profile> add -w <tgz> --offline --config.auto-install-peers=false` 把产出的 tarball 装进 profile。pnpm 的 `file:` 依赖让每个 profile 指向该部署副本。发布到 npm 的包则正常安装(它的 realpath 本来就在 profile 树内)。
65
+
66
+ ### 发布与 provenance
67
+
68
+ 发布预期带上 **npm provenance**:`publishConfig` 里带 `provenance: true`,因此从支持 OIDC 的 CI(GitHub Actions)发布即可,或显式执行 `npm publish --provenance --access public`。本机发布会缺少为该次构建作证的 OIDC provider,因此维护者从工作站发版时必须为那一次发布关掉 provenance,用 `--no-provenance`。**2FA 是发版前置条件**——provenance 不能替代它,且 npm 在账户/组织侧校验,本仓库无法验证。发布前把 `CHANGELOG.md` 的 `[Unreleased]` 落成即将发布的版本号(本次 `0.8.1` 就是这样做的)。
69
+
70
+ 发布形态必须从用 `--config.package-import-method=copy` 装出来的暂存目录打包:pnpm 默认的 import method 会把 store 里的文件硬链接过来,而 npmjs 会以 `415 Unsupported Media Type — Hard link is not allowed` 拒绝带硬链接成员的 tarball。`npm publish --dry-run` 抓不到这件事(dry run 不上传),因此发布 workflow 显式断言该形态,`plugins/fish-shell/tests/packed-files.spec.ts` 也对自己的 `npm pack` 产物做同一断言。
71
+
72
+ ## 交互式终端会话(L2)
73
+
74
+ 除了一次性的 `fish` 工具,bundle 还挂载了一个交互式终端 surface:一个真实 fish PTY,模型用 `terminal_open`、`terminal_send`、`terminal_read`、`terminal_signal`、`terminal_close` 与 `terminal_list` 来驱动它。它复用官方终端栈:后端 row 挂载的是 `terminal.js`——官方 `BashTerminalBackend` 的子类,与官方唯一的差别就是子进程环境里的 harness home 契约(见下方 **harness home 契约**);`spawn()` 完全继承,因此 argv 约束、sandbox-mode fence 与 `session.initialize()` 就绪语义都是官方代码。
75
+
76
+ patch 在 `cordis.patch.yml` 末尾追加了一个 `insert` group,id 为 `fish-terminal-group`:
77
+
78
+ | Row id | 包 | 配置 |
79
+ |---|---|---|
80
+ | `pty` | `@deepseek-ai/dsh-terminal` | — |
81
+ | `terminal-fish-pty` | `@banbolee/dsh-fish-shell/terminal` | `shellPath: fish`、`shellArgs: ["--no-config","-i","-C", <FISH_PROMPT_SETUP>]`、`timeoutMs: 300000` |
82
+ | `terminal-tools` | `@deepseek-ai/dsh-tool-terminal` | — |
83
+ | `fish-terminal-tools` | `@banbolee/dsh-fish-shell/terminal-tools` | `terminalTools: allow` |
84
+
85
+ group 行是 `cordis:group`,带 `group: true` 与 `isolate: { terminals: true }`:`terminals` service 必须在 group 内自足,因为挂在 host 平面的 row 会在所有 host 平面没有 `terminals` service 的 profile 里永远 pending。`tools` 刻意**不**隔离,因此 `terminal-tools` 注册的六个 `terminal_*` 工具会进入宿主 tools 注册表、对 agent 可见。
86
+
87
+ prompt setup 走 `-C` 参数(与 `terminal-fish.js` 导出的 `FISH_PROMPT_SETUP` 是同一个字符串):启动时不提交任何输入,所以 motd 保持干净;而把同一份 setup 从 PTY 输入喂进去,会被 fish line editor 回显出来。
88
+
89
+ **harness home 契约。** `terminal.js` 存在的唯一理由:PTY 子进程的环境是后端 overlay 加上 `scrubbedParentEnv()`,而后者会丢掉所有继承来的 `DSH_*` 名,所以过去在交互式会话里嵌套的 `dsh` 会把 home 解析成默认的 `~/.dsh`,而一次性 `fish` 工具用的却是真实的 harness home。该子类通过官方 constructor 的公开 `spawnTerminal` 缝(即 subprocess provider 在 ambient scrub **之后**才应用的那一层)补上 `DSH_HOME`,并在 context 能提供时补上 `DSH_PROFILE` / `DSH_PROFILE_DIR`,取值来自与一次性工具**同一个** registry:`ctx.shellEnv.collect()`。快照可以在 spawn 时取,是因为这三个 home 事实与执行无关——`collect` 只在加 `DSH_SESSION_ID` 时才读 execution——因此 `DSH_SHELL`、`DSH_SESSION_ID`、`DSH_PTY_SESSION_ID` 仍是官方 overlay 自己的值。除此之外没有任何差别:配置面就是原样 re-export 的官方 schema(`Config`),所以 `backendType` 保持默认 `shell`,也就是 `listBackends()` 报出的类型、`terminal_open { type: 'shell' }` 打开的类型。若 registry 完全没挂载(`dsh-base` 之外的自定义组合),同样这三个值会退到它们的公开来源——`resolveDshHome()` 与 `ctx.get('profileContext')`,即 registry 自己用的来源;而挂载着的 registry 就是权威来源:`collect()` 抛错会像一次性路径那样直接传播、而不是悄悄换成另一个 home,它的快照没声明的键也保持不声明、不从 ambient 环境或 profile context 借值。**维护注记:** 这一 row 属于我们,只是因为官方 row 没有 `env` 配置字段;一旦 `@deepseek-ai/dsh-terminal-bash` 支持了它,row 就回到官方包、`terminal.js` 会被删除。
90
+
91
+ `fish-terminal-tools` 是本插件为该 surface 新增的唯一入口:`terminal-tools.js` 里实现的 `terminalTools` 开关。它默认 `allow`(什么都不做,与部署中的其它工具一致);`deny` 会按 agent 收回这六个工具——见 **安全模型**。
92
+
93
+ **为什么一次性路径一行没动。** 追加的 group 是纯增量的。它上面的 host 平面 row 保持逐字不变(`bash-sandbox`/`tool-bash` 禁用,`fish-shell`/`tool-fish`/`fish-preset-policy` 插入),并且 `ctx.shell`(`index.js`)、一次性 `fish` 工具(`tool.js`)、`minimal` preset 的持久工具(`persistent.js`)与 `terminal-fish.js` 库都保持原样的代码路径:交互式 surface 的存在不改变一次性命令的任何行为。
94
+
95
+ **随包分发的 `@deepseek-ai/dsh-tool-terminal`。** `terminal-tools` row 从本包内部解析官方工具包,而不是从 profile 平面解析:`@deepseek-ai/dsh-tool-terminal@0.2.0-rc.2` 声明在 `dependencies` 与 `bundledDependencies` 里,所以打包会把它及其传递依赖作为实体文件放进 tarball 内的 `node_modules/` 下。这正是离线 profile 能安装成功的原因。它**不能**被挪到 `peerDependencies`:peer 解析会去找 profile 平面的副本,而那正是本设计要避免的。版本、来源与许可证见 `./THIRD-PARTY-NOTICES.md`。
96
+
97
+ ## 安全模型
98
+
99
+ - **默认:`terminalTools: 'allow'`。** 交互式 surface 默认开启,与部署中挂载的其它工具完全一样;装了本 bundle 的 fish profile 行为等同于任何带官方终端工具的 profile。`deny` 是退出开关:把 `fish-terminal-tools` row 的 config 设为 `terminalTools: deny`,之后在 `agent/created` 时、以及每次 `tools/change`,policy 都会通过官方 `agent.ctx.tools.restrict({ deny: [...] })` 按 agent 限制这六个名字。名字是逐个枚举的(从不用 `terminal_*` 通配),因此上游新增的第七个工具不会静默穿透策略。
100
+ - **六个工具**:`terminal_open`、`terminal_send`、`terminal_read`、`terminal_signal`、`terminal_close`、`terminal_list`。
101
+ - **不提供任何提权入口。** 本插件不注册自己的工具、不包装或替换任何宿主对象、不做 monkey-patch,也不提供 sudo/提权入口。会话进程就是 `fish`,权限等于启动该 profile 的用户权限。
102
+ - **沙箱继承。** 限制由宿主施加:`workspace-write` profile 会通过与其它命令相同的 sandbox 后端把交互式会话 fence 住(实测:写 `/etc` 被拒)。插件不旁路该 fence,也不把任何东西缓存到沙箱之外。
103
+ - **secret 处置与审计都有边界** —— 下方的 **已知限制** 写明了 hidden-input 边界、不支持的交互面与 best-effort 审计范围。
41
104
 
42
105
  ## 各 surface 的行为
43
106
 
@@ -49,7 +112,7 @@ dsh plugin --profile <name> add ./plugins/fish-shell
49
112
  - **One-shot**(standard、ptc、cordis、第三方 preset 与 headless):每次 shell 调用都会新起一个 non-login shell(对 `FishSandboxExecutor` 而言是在配置的 sandbox 后端下);调用之间不保留任何状态(cwd、变量、函数、历史)。
50
113
  - **Persistent**(`minimal` preset,它挂载了持久 PTY bash 机制):agent 的 `fish` 工具为每个 agent 维持一个存活的 fish session。当前目录、导出的变量与已定义的函数在调用之间保留;通过该工具运行的命令串行执行;超过工具 deadline 的命令会被打断并重置 shell(下一次调用会从 workspace 用一个全新的 shell 开始)。持久 fish PTY 由工具自己通过 `FishTerminalBackend` 启动并驱动(不涉及 `terminals` registry)。
51
114
  - 每条流的输出都受 executor 配置的上限约束;超时会被钳到 executor 的上限;模型看到的是 harness marker 契约。
52
- - `run_in_background: true` 会把长时间运行的命令作为后台 job 启动(`kind: fish`)并立即返回一个 job id;用 `job_output` 读取输出,用 `job_kill` 停止它。后台 job **不设 deadline**(工具以 `onExpiry: 'none'` 解析它,与 0.1.5 的 `start()` 及官方 `dsh-tool-bash` 一致):它会一直运行到 `job_kill`、取消或 composition teardown。需要组合 jobs service(`@deepseek-ai/dsh-jobs` + `@deepseek-ai/dsh-tool-jobs`);未组合时该工具会 fail loud。
115
+ - `run_in_background: true` 会把长时间运行的命令作为后台 job 启动(`kind: fish`)并立即返回一个 job id;用 `job_output` 读取输出,用 `job_kill` 停止它。后台 job **不设 deadline**(工具以 `onExpiry: 'none'` 解析它,与 0.1.5 的 `start()` 及官方 `dsh-tool-bash` 一致):它会一直运行到 `job_kill`、取消或 composition teardown。需要组合 jobs service(`@deepseek-ai/dsh-jobs` + `@deepseek-ai/dsh-tool-jobs`);未组合时该工具会 fail loud。**运行中的 job 被 kill/cancel 已由真实 PTY lane 验证**(`plugins/fish-shell/tests/terminal-session-real.spec.ts`):`job_output` 能读到仍然存活的 `terminal_send` job,`job_kill` 会把它收敛到终态 `killed`。
53
116
  - 在 sandboxing executor 下,被拒绝的命令可以在同一轮内用 `sandbox_permissions`(够用的最窄的更宽模式)加一句 `justification` 重新以更宽权限执行;这次重试弹出的 approval prompt 就是用户表示同意的方式。当 approval prompt 被禁用或升级被拒绝时,拒绝即为最终结果。
54
117
  - 要求 PATH 中有 `fish`;缺失时 executor 会 fail loud。
55
118
 
@@ -60,4 +123,38 @@ dsh plugin --profile <name> add ./plugins/fish-shell
60
123
  - **`minimal` 下的持久语义**由持久 `fish` 工具提供(cwd、导出的变量与已定义的函数在调用之间保留)。与官方持久 bash 一样,超出 deadline 的命令会被打断并重置 shell;agent 作用域的 `fish` 工具遮蔽了 host-global 的 one-shot 工具,因此升级字段(`sandbox_permissions`/`justification`)与后台执行(`run_in_background`)不属于持久 surface。
61
124
  - **第三方 preset 的边界**:policy 隐藏的是从 preset 继承来的 `bash` 同名工具。如果一个 preset 刻意排除全局 `fish` 工具(用 allowlist 把它过滤掉),或用别的名字注册自己的 shell 工具,那么它会被原样保留,只按 agent 给出一次警告——bundle 从不改动第三方 preset。
62
125
  - `fish` 工具面向模型的 surface 与官方 bash 工具对齐:后台执行(通过 `ctx.jobs` 的 `run_in_background`)、sandbox 升级(通过 `ctx.approval` 的 `sandbox_permissions`/`justification`)、终端/通用 UI 呈现,以及 `tool:fish` exit-code 提示词小节,全都支持。
126
+ - **secret:只有真 hidden-input 才是隐藏的。** 命令行、参数或 `terminal_send` 载荷对会话而言都是公开的:fish line editor 会回显收到的内容,会话也保留 scrollback,所以任何以非隐藏方式进入的内容都能被 `terminal_read` 读回。不要把 secret 作为命令参数或通过 `terminal_send` 传递;真正隐藏的读取(fish 里的 `read -s`)是唯一受支持、能让文本不出现在回显流里的方式。`stty -echo` **不是**处置方案:fish 的 line editor 会 repaint,哨兵仍留在 scrollback 里(实测)。
127
+ - **审计是 best-effort,不是生命周期承诺。** 插件只观察模型显式 `terminal_open`/`terminal_close` 调用能暴露的东西(session id、PID、status 迁移)。它**不**覆盖宿主侧回收、超时 kill、进程自己退出、别的 agent 或 owner 的会话,以及 group 内 `terminals` realm 的内部事件(在 host 平面不可见)。这里没有完整生命周期承诺:官方 `TerminalSessionService` 不暴露 open/exit/dispose 事件,插件也不自造审计面。
128
+ - **只支持文本交互。** 官方会话 API 不支持 `resize`、named key(方向键、具名 Ctrl 组合)、全屏 TUI(`vim`、`htop`)与 `TERM=dumb` 类降级终端;要支持它们只能自建 backend,不在范围内。
129
+ - **已验证的 L2 行为。** 下列每一条在交互式 surface 首次成文时都还是未决问题;现在每一条都有 lane 断言其记录行为(lane 索引见 **测试覆盖矩阵**):
130
+ - **preset 组合。** 官方 `minimal` preset 下两个 `terminals` realm 共存——host L2 group 的(fish)与 preset 自己的 persistent-shell group 的(`/bin/bash`)——六个 `terminal_*` 工具对 preset 绑定的 agent 恰好各出现一次;`dsh-tui` 组合挂载同一个 group,并跑通 `open`/`send`/`read`/`close`(`tests/composition/fish-pty-minimal.spec.ts`、`tests/composition/fish-pty-dsh-tui.spec.ts`)。
131
+ - **活动会话时切 mode。** 切 standing policy 会被**同步**拒绝,报 `cannot change sandbox mode … while persistent terminal sessions are open`:mode 事件未提交、生效 mode 不变、原会话仍可用;会话关闭后同一调用成功(`plugins/fish-shell/tests/terminal-session-real.spec.ts`)。
132
+ - **`read-only` 模式。** `read-only` 是文件效果模式,不是能力开关:六个工具仍然可见、PTY 仍能 spawn(motd `dsh>`),但解析后的 policy 完全没有 writable root,因此连 session workspace 内的写入也被拒绝——`read-only` 下 workspace 不是写边界(同一条 lane)。
133
+ - **跨 agent 的 owner 隔离。** 同 profile 的第二个 agent 看到空的 `terminal_list`,且读、写、关都不行:工具层报 `belongs to another agent`,背后的 registry code 是 `FOREIGN_SESSION`;owner 自己的会话毫发无损(同一条 lane)。
134
+ - **profile patch 直引 bundled 包名。** 该名字**只**从本 bundle 内部解析(`…/@banbolee/dsh-fish-shell/node_modules/@deepseek-ai/dsh-tool-terminal`),永远不来自 dsh 安装目录;解析不等于激活——因为 group 把 `terminals` isolate 了,该 row 停在 `pending (waiting for service: terminals)`,所以六个工具不会被注册第二次,且 boot 会把这行 pending 报到 stderr 而不是吞掉(`tests/composition/fish-pty-bundled-row.spec.ts`)。
135
+ - **未验证(不要当成已支持)**:
136
+ - `FishTerminalBackend` 的 submitted-setup 路径在 fish 3.7.1 上收敛:该库没有被 patch 挂载,因此不影响 L2 surface,老持久路径继续由它自己的 spec 覆盖;
137
+ - 完全无网的冷环境首装:打包 lane(`scripts/qa/fish-packed-offline.mjs`)确实用 `--offline` 安装,但它的 store 是先被仓库安装预热过的。
63
138
  - 仅支持 POSIX:`fish` 二进制与底层 process-group 语义在 Windows 上不可用(Windows 由 pwsh 系列覆盖)。
139
+
140
+ ## 排障
141
+
142
+ ### `@deepseek-ai/dsh-tool-terminal` 报 `ERR_MODULE_NOT_FOUND`
143
+
144
+ bundled 的工具包没有进安装产物,或它被当成 peer(profile 平面)依赖解析了。用 hoisted linker 构建:先 `pnpm install --prod --config.node-linker=hoisted`,再 `pnpm pack --config.node-linker=hoisted`。不带它 pnpm 会拒绝打包 bundled 依赖,并报 `ERR_PNPM_BUNDLED_DEPENDENCIES_WITHOUT_HOISTED`。检查产物:tarball 必须包含 `package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json`。有一条上游原因不在我们能修的范围内:`@deepseek-ai/dsh-tool-terminal@0.2.0-rc.2` 声明的 `exports` 里有 `./src/*`,而实际发布的 tarball 并不包含它,因此任何按该子路径解析的代码都会 `ERR_MODULE_NOT_FOUND`。
145
+
146
+ ### `ERR_PNPM_NO_OFFLINE_TARBALL`
147
+
148
+ 离线安装打在了冷 pnpm store 上:`--offline` 下 pnpm 拒绝为它没有缓存的 tarball 走网络。先用一次联网安装把 store 预热(或首次安装时不带 `--offline` 跑同一条 `dsh plugin ... add`),然后再跑离线命令。插件自身的运行时依赖是随插件 tarball 一起走的,所以这是 store 状态问题,不是 bundled 文件缺失。
149
+
150
+ ### 交互式会话里嵌套的 `dsh`
151
+
152
+ 从本 bundle 起,会话会自动带上 harness home 契约:它的子进程环境里有 `DSH_HOME`(当 profile context 能提供时还有 `DSH_PROFILE` / `DSH_PROFILE_DIR`),取值来自与一次性 `fish` 工具同一个 `ctx.shellEnv` registry,因此两个 surface 不可能漂移——具体位置与理由见上方 **harness home 契约**。真实 lane 断言了这一结果:会话里 `echo $DSH_HOME` 打印的就是本 profile 的 home,`dsh --profile <profile> --dump-config` 组合出该 profile 的树,而同一个命令换到陌生 `DSH_HOME` 下会失败。因此在会话里嵌套启动的 `dsh` 会与打开它的会话跑在同一个 home、同一套插件上。
153
+
154
+ 老版本 bundle——以及任何 `terminal-fish-pty` row 仍指向官方 `@deepseek-ai/dsh-terminal-bash` 的部署——没有这份契约,而且对它们来说没有只改配置的解法:那个 row 没有 `env` 配置字段,写在它上面的 `env:` 会被 schema 收下、然后被静默忽略;后端的 `childEnvironment()` 也是 module-private 的。那里的嵌套 `dsh` 会把 home 解析成 `~/.dsh`,那里可能存着旧版本的 profile 与插件状态——残留的旧插件会让嵌套启动失败,或让它的 row 被禁用。正确做法是显式传入 home——把 `/path/to/dsh-home` 换成外层会话启动时使用的 Harness home(用一次 `fish` 调用即可打印:`echo $DSH_HOME`):
155
+
156
+ ```
157
+ env -u DSH_SESSION_ID -u DSH_PTY_SESSION_ID -u DSH_SHELL -u DSH_PERMISSION_MODE DSH_HOME=/path/to/dsh-home dsh --profile <profile>
158
+ ```
159
+
160
+ **维护注记。** 自己拥有这个 row(`terminal.js`)是我们能在不改官方包的前提下加上契约的唯一原因;一旦 `@deepseek-ai/dsh-terminal-bash` 支持了 `env` 配置字段(或 registry 自己转发 home 事实),row 就回到官方包、`terminal.js` 会被删除,本节只剩第一段——真实 lane 的断言在两种实现下都保留。那次退休同样要更新 `tests/docs-shape-fish-shell.spec.ts` 里硬编码的 `NESTED_DSH_ENTRY` 契约,因为它点名了这个模块。
@@ -0,0 +1,91 @@
1
+ # Third-party notices — `@banbolee/dsh-fish-shell`
2
+
3
+ This package distributes third-party code inside its tarball. The runtime
4
+ dependency listed under `bundledDependencies` in `package.json` — and its
5
+ transitive dependencies — are packed as real files under
6
+ `package/node_modules/**` by the release chain
7
+ (`pnpm install --prod --config.node-linker=hoisted` then
8
+ `pnpm pack --config.node-linker=hoisted`), so an offline profile can install
9
+ the bundle without fetching them separately.
10
+
11
+ This file records the origin, version and license of each bundled package. It
12
+ is a human-readable provenance note, **not** a replacement for the license
13
+ texts: every bundled package ships its own `LICENSE` file inside the tarball,
14
+ and that packaged file is the authoritative text. The license names below come
15
+ from the packaged `LICENSE` file and the package's own `package.json`; where
16
+ either was ambiguous this file would say "see the packaged LICENSE" instead of
17
+ naming one.
18
+
19
+ ## Bundled inventory
20
+
21
+ | Package | Version | License | Location inside the tarball |
22
+ |---|---|---|---|
23
+ | `@deepseek-ai/dsh-tool-terminal` | `0.2.0-rc.2` | MIT | `package/node_modules/@deepseek-ai/dsh-tool-terminal/` |
24
+ | `@deepseek-ai/schemastery` | `3.18.4` | MIT | `package/node_modules/@deepseek-ai/schemastery/` |
25
+ | `@deepseek-ai/cosmokit` | `1.8.5` | MIT | `package/node_modules/@deepseek-ai/cosmokit/` |
26
+ | `@standard-schema/spec` | `1.1.0` | MIT | `package/node_modules/@standard-schema/spec/` |
27
+
28
+ Dependency edges, as declared by the packaged manifests:
29
+
30
+ - `@deepseek-ai/dsh-tool-terminal@0.2.0-rc.2` → `@deepseek-ai/schemastery ~3.18.4`
31
+ - `@deepseek-ai/schemastery@3.18.4` → `@standard-schema/spec ^1.1.0`, `@deepseek-ai/cosmokit ~1.8.5`
32
+ - `@standard-schema/spec@1.1.0` and `@deepseek-ai/cosmokit@1.8.5` → no runtime dependencies
33
+
34
+ Those four packages are the complete `node_modules/` inventory of the packed
35
+ tarball; each of them includes its `LICENSE` file.
36
+
37
+ ## `@deepseek-ai/dsh-tool-terminal` 0.2.0-rc.2 — MIT
38
+
39
+ - Origin: `git+https://github.com/deepseek-ai/deepseek-harness.git`, directory
40
+ `packages/terminal/tool-terminal` (from the packaged `package.json`
41
+ `repository`).
42
+ - License: MIT (`package.json` `"license": "MIT"`), license text header
43
+ "MIT License", copyright notice `Copyright (c) 2026 DeepSeek`.
44
+ - Role here: the six model-facing `terminal_*` tools mounted by the
45
+ `terminal-tools` row of this bundle's `cordis.patch.yml`. It is declared as a
46
+ `dependencies` + `bundledDependencies` entry of this package, not as a peer,
47
+ so it resolves from inside this package.
48
+
49
+ ## `@deepseek-ai/schemastery` 3.18.4 — MIT
50
+
51
+ - Origin: `git+https://github.com/deepseek-ai/deepseek-harness.git`, directory
52
+ `vendor/schemastery` (from the packaged `package.json` `repository`).
53
+ - License: MIT (`package.json` `"license": "MIT"`), license text header
54
+ "MIT License", copyright notice `Copyright (c) 2021-present Shigma`
55
+ (packaged `author`: `Shigma <shigma10826@gmail.com>`).
56
+ - Role here: transitive runtime dependency of
57
+ `@deepseek-ai/dsh-tool-terminal`.
58
+
59
+ ## `@deepseek-ai/cosmokit` 1.8.5 — MIT
60
+
61
+ - Origin: `git+https://github.com/deepseek-ai/deepseek-harness.git`, directory
62
+ `vendor/cosmokit` (from the packaged `package.json` `repository`).
63
+ - License: MIT (`package.json` `"license": "MIT"`), license text header
64
+ "MIT License", copyright notice `Copyright (c) 2021-present Shigma`
65
+ (packaged `author`: `Shigma <shigma10826@gmail.com>`).
66
+ - Role here: transitive runtime dependency of `@deepseek-ai/schemastery`.
67
+
68
+ ## `@standard-schema/spec` 1.1.0 — MIT
69
+
70
+ - Origin: `https://github.com/standard-schema/standard-schema` (from the
71
+ packaged `package.json` `repository`; packaged `homepage`:
72
+ `https://standardschema.dev`).
73
+ - License: MIT (`package.json` `"license": "MIT"`), license text header
74
+ "MIT License", copyright notice `Copyright (c) 2024 Colin McDonnell`
75
+ (packaged `author`: `Colin McDonnell`).
76
+ - Role here: transitive runtime dependency of `@deepseek-ai/schemastery`.
77
+
78
+ ## Re-verifying this inventory
79
+
80
+ After packing, list the tarball and check the four packages (and their
81
+ `LICENSE` files) are present:
82
+
83
+ ```sh
84
+ pnpm install --prod --config.node-linker=hoisted
85
+ pnpm pack --config.node-linker=hoisted
86
+ tar tzf banbolee-dsh-fish-shell-*.tgz | grep '^package/node_modules/'
87
+ ```
88
+
89
+ Version bumps of the bundled dependency must update this file together with
90
+ `dependencies` in `package.json` (the version pin is asserted by the
91
+ plugin's contract tests).
package/cordis.patch.yml CHANGED
@@ -52,3 +52,59 @@
52
52
 
53
53
  - id: fish-preset-policy
54
54
  name: '@banbolee/dsh-fish-shell/policy'
55
+ # ── L2 interactive terminal sessions (fish-shell-tty-v3 §3.1) ───────────────────
56
+ # A self-contained group: `isolate.terminals` keeps the `terminals` service
57
+ # entry-local to the group (mounting it at the host plane would leave the entry
58
+ # pending in profiles whose host plane has no `terminals` service). Inside it:
59
+ # - `pty` the official terminal registry;
60
+ # - `terminal-fish-pty` this bundle's `terminal` module: the OFFICIAL
61
+ # `@deepseek-ai/dsh-terminal-bash` backend, driven with fish argv, PLUS the
62
+ # harness home contract (`DSH_HOME`, `DSH_PROFILE`, `DSH_PROFILE_DIR`) in
63
+ # the PTY child environment — the same facts the one-shot `fish` tool gets
64
+ # from `ctx.shellEnv`, so a nested `dsh` resolves the real harness home
65
+ # instead of `~/.dsh`. The row used to mount the official package directly;
66
+ # it cannot any more because that row has no `env` config field and its
67
+ # `childEnvironment()` is module-private, so owning the row is the only way
68
+ # to add the contract without editing or patching the official module.
69
+ # Everything else is still the official class (see `terminal.js`): the
70
+ # backend type stays `shell`, argv confinement and the sandbox-mode fence
71
+ # stay official, and readiness still comes from `session.initialize()`.
72
+ # The prompt setup rides in `-C` (startup submits nothing) so the motd stays
73
+ # clean — feeding the same setup through PTY input gets it echoed back by
74
+ # the fish line editor. The string below is FISH_PROMPT_SETUP from
75
+ # `terminal-fish.js` verbatim: the `\x1b` / `\x07` escapes must reach fish
76
+ # as literal backslash sequences for its `printf` to build the bytes at
77
+ # runtime (a contract test compares this value with the exported constant);
78
+ # - `terminal-tools` the official tool package; `tools` is NOT isolated,
79
+ # so its six `terminal_*` tools register into the host registry and are
80
+ # visible to agents;
81
+ # - `fish-terminal-tools` this bundle's `terminalTools` policy, mounted in the
82
+ # same group so its scope matches the session surface (`deny` restricts the
83
+ # six explicit names per agent; `allow` is the default no-op).
84
+ # The host-plane rows above (bash disabled + fish-shell/tool-fish/
85
+ # fish-preset-policy) are untouched: `ctx.shell` and the one-shot `fish` tool
86
+ # keep their exact code path.
87
+ - insert:
88
+ - id: fish-terminal-group
89
+ name: 'cordis:group'
90
+ group: true
91
+ isolate:
92
+ terminals: true
93
+ config:
94
+ - id: pty
95
+ name: '@deepseek-ai/dsh-terminal'
96
+
97
+ - id: terminal-fish-pty
98
+ name: '@banbolee/dsh-fish-shell/terminal'
99
+ config:
100
+ shellPath: fish
101
+ shellArgs: ["--no-config","-i","-C","function fish_greeting; end; function fish_prompt; printf '\\x1b]133;D;%s\\x07' $status; echo -n 'dsh> '; end; function fish_right_prompt; end;"]
102
+ timeoutMs: 300000
103
+
104
+ - id: terminal-tools
105
+ name: '@deepseek-ai/dsh-tool-terminal'
106
+
107
+ - id: fish-terminal-tools
108
+ name: '@banbolee/dsh-fish-shell/terminal-tools'
109
+ config:
110
+ terminalTools: allow
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-present Shigma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # cosmokit
2
+
3
+ [![Codecov](https://img.shields.io/codecov/c/github/shigma/cosmokit?style=flat-square)](https://codecov.io/gh/shigma/cosmokit)
4
+ [![npm](https://img.shields.io/npm/v/cosmokit?style=flat-square)](https://www.npmjs.com/package/cosmokit)
5
+
6
+ A collection of common utilities.
7
+
8
+ ## Usage
9
+
10
+ ### Node.js
11
+
12
+ ```sh
13
+ npm install cosmokit
14
+ ```
15
+
16
+ ```ts
17
+ import cosmokit from 'cosmokit'
18
+ ```
19
+
20
+ ### Deno
21
+
22
+ ```ts
23
+ import cosmokit from 'npm:cosmokit@latest'
24
+ ```
25
+
26
+ ## Config references
27
+
28
+ `Volatile<T>` exposes `get()` for immutable data snapshots. `createVolatile()` copies and freezes validated data; framework-owned `updateVolatile()` transfers a candidate snapshot into an existing reference. Framework-owned `volatileEntries()` enumerates references and their paths through plain config containers, stopping at snapshots and opaque objects. The shared symbol protocol recognizes references across ESM/CJS copies. Plugin consumers import the type from Cordis and retain the reference rather than caching values between operations.
29
+
30
+ `deepEqual()` treats two volatile references as equal regardless of their snapshots. Strict comparisons distinguish null from undefined recursively, compare URLs by normalized `href` and other opaque objects by identity, and treat distinct cyclic structures as unequal. Array comparison visits every index and treats holes as undefined. Snapshot-change detection must compare `.get()` results separately.