@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.2

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 (238) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -2
  4. package/config/edge.example.json +179 -0
  5. package/config/privacy-policy.json +202 -0
  6. package/cordis.desktop-safe.patch.yml +129 -0
  7. package/cordis.patch.yml +382 -0
  8. package/cordis.yml +374 -0
  9. package/lib/attestation.d.ts +2 -0
  10. package/lib/attestation.d.ts.map +1 -0
  11. package/lib/attestation.js +2 -0
  12. package/lib/audit-log.d.ts +2 -0
  13. package/lib/audit-log.d.ts.map +1 -0
  14. package/lib/audit-log.js +2 -0
  15. package/lib/client.js +87 -0
  16. package/lib/cloud-llm-adapter.d.ts +2 -0
  17. package/lib/cloud-llm-adapter.d.ts.map +1 -0
  18. package/lib/cloud-llm-adapter.js +2 -0
  19. package/lib/dsh-adapter/attestation.d.ts +42 -0
  20. package/lib/dsh-adapter/attestation.d.ts.map +1 -0
  21. package/lib/dsh-adapter/attestation.js +40 -0
  22. package/lib/dsh-adapter/audit-log.d.ts +139 -0
  23. package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
  24. package/lib/dsh-adapter/audit-log.js +376 -0
  25. package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
  26. package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
  27. package/lib/dsh-adapter/cloud-egress.js +257 -0
  28. package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
  29. package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
  30. package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
  31. package/lib/dsh-adapter/contract.d.ts +80 -0
  32. package/lib/dsh-adapter/contract.d.ts.map +1 -0
  33. package/lib/dsh-adapter/contract.js +194 -0
  34. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
  35. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
  36. package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
  37. package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
  38. package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
  39. package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
  40. package/lib/dsh-adapter/edge-config.d.ts +484 -0
  41. package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
  42. package/lib/dsh-adapter/edge-config.js +898 -0
  43. package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
  44. package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
  45. package/lib/dsh-adapter/edge-context-view.js +483 -0
  46. package/lib/dsh-adapter/effective-model.d.ts +85 -0
  47. package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
  48. package/lib/dsh-adapter/effective-model.js +171 -0
  49. package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
  50. package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
  51. package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
  52. package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
  53. package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
  54. package/lib/dsh-adapter/etscli-tools.js +393 -0
  55. package/lib/dsh-adapter/file-logger.d.ts +294 -0
  56. package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
  57. package/lib/dsh-adapter/file-logger.js +564 -0
  58. package/lib/dsh-adapter/function-call-external.d.ts +357 -0
  59. package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
  60. package/lib/dsh-adapter/function-call-external.js +1005 -0
  61. package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
  62. package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
  63. package/lib/dsh-adapter/host-egress-guard.js +213 -0
  64. package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
  65. package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
  66. package/lib/dsh-adapter/host-llm-trace.js +344 -0
  67. package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
  68. package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
  69. package/lib/dsh-adapter/http-api-routes.js +79 -0
  70. package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
  71. package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
  72. package/lib/dsh-adapter/http-api-sessions.js +173 -0
  73. package/lib/dsh-adapter/http-api.d.ts +221 -0
  74. package/lib/dsh-adapter/http-api.d.ts.map +1 -0
  75. package/lib/dsh-adapter/http-api.js +1811 -0
  76. package/lib/dsh-adapter/index.d.ts +62 -0
  77. package/lib/dsh-adapter/index.d.ts.map +1 -0
  78. package/lib/dsh-adapter/index.js +97 -0
  79. package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
  80. package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
  81. package/lib/dsh-adapter/input-guard-messages.js +136 -0
  82. package/lib/dsh-adapter/input-guard.d.ts +508 -0
  83. package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
  84. package/lib/dsh-adapter/input-guard.js +2174 -0
  85. package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
  86. package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
  87. package/lib/dsh-adapter/intent-prompts.js +97 -0
  88. package/lib/dsh-adapter/intent-router.d.ts +710 -0
  89. package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
  90. package/lib/dsh-adapter/intent-router.js +2802 -0
  91. package/lib/dsh-adapter/intent-signals.d.ts +208 -0
  92. package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
  93. package/lib/dsh-adapter/intent-signals.js +391 -0
  94. package/lib/dsh-adapter/invariant-install.d.ts +16 -0
  95. package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
  96. package/lib/dsh-adapter/invariant-install.js +15 -0
  97. package/lib/dsh-adapter/invariant.d.ts +16 -0
  98. package/lib/dsh-adapter/invariant.d.ts.map +1 -0
  99. package/lib/dsh-adapter/invariant.js +22 -0
  100. package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
  101. package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
  102. package/lib/dsh-adapter/llm-adapters.js +16 -0
  103. package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
  104. package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
  105. package/lib/dsh-adapter/llm-internal-caller.js +100 -0
  106. package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
  107. package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
  108. package/lib/dsh-adapter/llm-max-turns.js +127 -0
  109. package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
  110. package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
  111. package/lib/dsh-adapter/llm-trace-store.js +205 -0
  112. package/lib/dsh-adapter/llm-trace.d.ts +450 -0
  113. package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
  114. package/lib/dsh-adapter/llm-trace.js +463 -0
  115. package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
  116. package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
  117. package/lib/dsh-adapter/local-llm-adapter.js +787 -0
  118. package/lib/dsh-adapter/local-storage.d.ts +96 -0
  119. package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
  120. package/lib/dsh-adapter/local-storage.js +198 -0
  121. package/lib/dsh-adapter/model-router.d.ts +79 -0
  122. package/lib/dsh-adapter/model-router.d.ts.map +1 -0
  123. package/lib/dsh-adapter/model-router.js +107 -0
  124. package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
  125. package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
  126. package/lib/dsh-adapter/ohttp-gateway.js +32 -0
  127. package/lib/dsh-adapter/openai-sse.d.ts +85 -0
  128. package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
  129. package/lib/dsh-adapter/openai-sse.js +462 -0
  130. package/lib/dsh-adapter/pii-utils.d.ts +96 -0
  131. package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
  132. package/lib/dsh-adapter/pii-utils.js +324 -0
  133. package/lib/dsh-adapter/plugin-host.d.ts +38 -0
  134. package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
  135. package/lib/dsh-adapter/plugin-host.js +38 -0
  136. package/lib/dsh-adapter/plugin.d.ts +46 -0
  137. package/lib/dsh-adapter/plugin.d.ts.map +1 -0
  138. package/lib/dsh-adapter/plugin.js +100 -0
  139. package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
  140. package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
  141. package/lib/dsh-adapter/privacy-policy.js +391 -0
  142. package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
  143. package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
  144. package/lib/dsh-adapter/prompt-guard.js +872 -0
  145. package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
  146. package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
  147. package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
  148. package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
  149. package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
  150. package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
  151. package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
  152. package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
  153. package/lib/dsh-adapter/secure-bridge.js +35 -0
  154. package/lib/dsh-adapter/session-attribution.d.ts +70 -0
  155. package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
  156. package/lib/dsh-adapter/session-attribution.js +76 -0
  157. package/lib/dsh-adapter/sessions.d.ts +86 -0
  158. package/lib/dsh-adapter/sessions.d.ts.map +1 -0
  159. package/lib/dsh-adapter/sessions.js +179 -0
  160. package/lib/dsh-adapter/tcm.d.ts +55 -0
  161. package/lib/dsh-adapter/tcm.d.ts.map +1 -0
  162. package/lib/dsh-adapter/tcm.js +94 -0
  163. package/lib/dsh-adapter/td-bus.d.ts +87 -0
  164. package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
  165. package/lib/dsh-adapter/td-bus.js +196 -0
  166. package/lib/dsh-adapter/tool-runner.d.ts +185 -0
  167. package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
  168. package/lib/dsh-adapter/tool-runner.js +271 -0
  169. package/lib/dsh-adapter/tools.d.ts +118 -0
  170. package/lib/dsh-adapter/tools.d.ts.map +1 -0
  171. package/lib/dsh-adapter/tools.js +292 -0
  172. package/lib/dsh-adapter/types.d.ts +23 -0
  173. package/lib/dsh-adapter/types.d.ts.map +1 -0
  174. package/lib/dsh-adapter/types.js +16 -0
  175. package/lib/dsh-adapter/voice-input.d.ts +30 -0
  176. package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
  177. package/lib/dsh-adapter/voice-input.js +34 -0
  178. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
  179. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
  180. package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
  181. package/lib/http-api.d.ts +2 -0
  182. package/lib/http-api.d.ts.map +1 -0
  183. package/lib/http-api.js +2 -0
  184. package/lib/index.d.ts +48 -0
  185. package/lib/index.d.ts.map +1 -0
  186. package/lib/index.js +49 -0
  187. package/lib/intent-router.d.ts +2 -0
  188. package/lib/intent-router.d.ts.map +1 -0
  189. package/lib/intent-router.js +2 -0
  190. package/lib/llm-adapters.d.ts +2 -0
  191. package/lib/llm-adapters.d.ts.map +1 -0
  192. package/lib/llm-adapters.js +2 -0
  193. package/lib/local-llm-adapter.d.ts +2 -0
  194. package/lib/local-llm-adapter.d.ts.map +1 -0
  195. package/lib/local-llm-adapter.js +2 -0
  196. package/lib/local-storage.d.ts +2 -0
  197. package/lib/local-storage.d.ts.map +1 -0
  198. package/lib/local-storage.js +2 -0
  199. package/lib/model-router.d.ts +2 -0
  200. package/lib/model-router.d.ts.map +1 -0
  201. package/lib/model-router.js +2 -0
  202. package/lib/ohttp-gateway.d.ts +2 -0
  203. package/lib/ohttp-gateway.d.ts.map +1 -0
  204. package/lib/ohttp-gateway.js +2 -0
  205. package/lib/plugin-host.d.ts +2 -0
  206. package/lib/plugin-host.d.ts.map +1 -0
  207. package/lib/plugin-host.js +2 -0
  208. package/lib/privacy-policy.d.ts +2 -0
  209. package/lib/privacy-policy.d.ts.map +1 -0
  210. package/lib/privacy-policy.js +2 -0
  211. package/lib/prompt-guard.d.ts +2 -0
  212. package/lib/prompt-guard.d.ts.map +1 -0
  213. package/lib/prompt-guard.js +2 -0
  214. package/lib/sanitizer-pipeline.d.ts +2 -0
  215. package/lib/sanitizer-pipeline.d.ts.map +1 -0
  216. package/lib/sanitizer-pipeline.js +2 -0
  217. package/lib/secure-bridge.d.ts +2 -0
  218. package/lib/secure-bridge.d.ts.map +1 -0
  219. package/lib/secure-bridge.js +2 -0
  220. package/lib/sessions.d.ts +2 -0
  221. package/lib/sessions.d.ts.map +1 -0
  222. package/lib/sessions.js +2 -0
  223. package/lib/tcm.d.ts +2 -0
  224. package/lib/tcm.d.ts.map +1 -0
  225. package/lib/tcm.js +2 -0
  226. package/lib/td-bus.d.ts +2 -0
  227. package/lib/td-bus.d.ts.map +1 -0
  228. package/lib/td-bus.js +2 -0
  229. package/lib/tools.d.ts +2 -0
  230. package/lib/tools.d.ts.map +1 -0
  231. package/lib/tools.js +2 -0
  232. package/lib/voice-input.d.ts +2 -0
  233. package/lib/voice-input.d.ts.map +1 -0
  234. package/lib/voice-input.js +2 -0
  235. package/package.json +253 -3
  236. package/presets/agent-preset-edge.json +50 -0
  237. package/presets/edge.desktop-safe.patch.yml +503 -0
  238. package/presets/edge.patch.yml +557 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * etscli-tools — §7.98:把 14 个 **只读** etscli 子命令注册进 dsh 工具注册表。
3
+ *
4
+ * ## 为什么有这个插件
5
+ *
6
+ * 在此之前,47 个 `etscli_*` 工具**只存在于 `scripts/etscli-tools-test.py` 的
7
+ * `_TOOL_SPEC` 硬编码列表里**,直接 POST 给 needle 的裸 `/extract`。
8
+ * `ctx.tools` 里一个都没有 —— 这意味着:
9
+ *
10
+ * - `realm-skill-bridge.getToolSchemasForExternal()` 读到的是上游那套
11
+ * (bash/fs/web/...),**从没见过 etscli 工具**
12
+ * - needle 选出的任何 etscli 工具都会撞 `tool-runner` 的 schema 面校验 →
13
+ * `external.unknown_tool` → denied → `executedCount` 恒为 0
14
+ * - 于是 `function-call-external` 的 `buildSynthesisContext()` **永远不触发**,
15
+ * §7.96 Track B 的 synthesis few-shot 在真实部署里是不可达的死代码
16
+ *
17
+ * 本插件补上这一层,让「needle 选工具 → tool-runner 执法 → 真实执行 → synthesis
18
+ * → LOCAL LLM 出回复」这条链路第一次真正跑通。
19
+ *
20
+ * ## 安全边界(本文件最重要的约束)
21
+ *
22
+ * **只注册只读工具。** 47 个 etscli 工具里 27 个有真实副作用,其中几个不可逆:
23
+ *
24
+ * | 排除的工具 | help 原文依据 |
25
+ * |---|---|
26
+ * | `auth logout` | "Remove stored credentials" —— 抹掉凭据,其他工具全砖 |
27
+ * | `fs rm` | `--delete-type int ... (default 2)`,2 = **permanent**,不是回收站 |
28
+ * | `file delete` | 逗号分隔批量删除,**无确认** |
29
+ * | `chat send` / `send-file` | 向真实群发**不可撤回**的消息 |
30
+ * | `migrate verify` | "Missing records ... trigger an **automatic re-transfer**" —— **名字像只读,实为写操作** |
31
+ * | `migrate transfer` | 写 `~/.etscli/migrated.json` + 批量传输 |
32
+ * | `sharing create` | 生成**公开**分享链接 |
33
+ * | `meeting summary` | "Trigger meeting AI summary" —— 触发服务端 AI,计费 |
34
+ * | `file download` | 远端只读,但**落本地盘** |
35
+ * | 其余写类 | 见 `docs/PROGRESS.md` §7.98 排除表 |
36
+ *
37
+ * 「不注册」= 不进 `ctx.tools` = 不出现在 `getToolSchemasForExternal()` =
38
+ * **needle 物理上无法选中**。白名单就是注册表本身,不依赖运行期拦截。
39
+ *
40
+ * ## 上游包的 import 纪律
41
+ *
42
+ * **不 import `@deepseek-ai/dsh-tools`。** 该包只存在于 dsh CLI 自带的
43
+ * `node_modules`(`/home/<user>/.npm-global/lib/node_modules/@deepseek-ai/dsh/...`),
44
+ * **不在 dsh-edge profile 的 `node_modules` 里** —— 从本插件 `lib/` 出发做
45
+ * runtime import 会解析失败。本文件因此改用结构化类型 + 窄 cast 访问
46
+ * `ctx.tools` / `ctx.shell`,与 `tools.ts:275-291`、`tool-runner.ts` 文件头
47
+ * 声明的同款纪律一致(全仓零 `import ... from '@deepseek-ai/dsh-tools'` 的值导入)。
48
+ *
49
+ * @module dsh-edge-agent/etscli-tools
50
+ */
51
+ import Schema from '@deepseek-ai/schemastery';
52
+ import { loadEdgeConfig } from './edge-config.js';
53
+ /**
54
+ * 14 个只读工具。
55
+ *
56
+ * ⚠️ **flag 映射刻意不等于模型侧参数名** —— 真实 CLI 的 flag 与
57
+ * `etscli-tools-test.py` 的抽象参数名有出入,照抄参数名会静默返回空结果。
58
+ * 实测依据(2026-09-30,`etscli <svc> --help`):
59
+ *
60
+ * | 工具 | 模型侧参数 | 真实 flag | 备注 |
61
+ * |---|---|---|---|
62
+ * | `fs ls` | `path` | `--parent` | 不是 `--path` |
63
+ * | `fs stats` | `path` | `--parent` | 同上 |
64
+ * | `fs stat` | `path` | `--file-id` | help: "File ID (file_uri or path)" |
65
+ * | `fs search` | `name` | `--name` | **CLI 侧必填** |
66
+ * | `file search` | `keyword` | `--keyword` | |
67
+ * | `system alarm list` | `limit` | `--number` | 不是 `--limit` |
68
+ */
69
+ const ETSCLI_TOOL_SPECS = [{
70
+ name: 'etscli_auth_list',
71
+ description: 'list all saved login profiles',
72
+ argv: ['auth', 'list'],
73
+ params: [],
74
+ },
75
+ {
76
+ name: 'etscli_auth_status',
77
+ description: 'show the authentication status of the current profile',
78
+ argv: ['auth', 'status'],
79
+ params: [],
80
+ },
81
+ {
82
+ name: 'etscli_user_list',
83
+ description: 'list all users on the box',
84
+ argv: ['user', 'list'],
85
+ params: [],
86
+ },
87
+ {
88
+ name: 'etscli_system_info',
89
+ description: 'show system information of the box',
90
+ argv: ['system', 'info'],
91
+ params: [],
92
+ },
93
+ {
94
+ name: 'etscli_system_health',
95
+ description: 'check the health of the box',
96
+ argv: ['system', 'health'],
97
+ params: [],
98
+ },
99
+ {
100
+ name: 'etscli_system_alarm_list',
101
+ description: 'list alarms on the box',
102
+ argv: ['system', 'alarm', 'list'],
103
+ params: [
104
+ { model: 'limit', flag: '--number', type: 'integer', description: 'number of alarms to fetch (0 = all)' },
105
+ ],
106
+ },
107
+ {
108
+ name: 'etscli_file_search',
109
+ description: 'search box files by keyword',
110
+ argv: ['file', 'search'],
111
+ params: [
112
+ { model: 'keyword', flag: '--keyword', type: 'string', description: 'keyword search', required: true },
113
+ ],
114
+ },
115
+ {
116
+ name: 'etscli_fs_ls',
117
+ description: 'list files in a directory',
118
+ argv: ['fs', 'ls'],
119
+ params: [
120
+ { model: 'path', flag: '--parent', type: 'string', description: 'parent folder path (e.g. "/")' },
121
+ ],
122
+ },
123
+ {
124
+ name: 'etscli_fs_stat',
125
+ description: 'show stat info of a file',
126
+ argv: ['fs', 'stat'],
127
+ params: [
128
+ { model: 'path', flag: '--file-id', type: 'string', description: 'file URI or path', required: true },
129
+ ],
130
+ },
131
+ {
132
+ name: 'etscli_fs_stats',
133
+ description: 'show statistics of a directory',
134
+ argv: ['fs', 'stats'],
135
+ params: [
136
+ { model: 'path', flag: '--parent', type: 'string', description: 'parent folder path', required: true },
137
+ ],
138
+ },
139
+ {
140
+ name: 'etscli_fs_search',
141
+ description: 'search files by name under a directory',
142
+ argv: ['fs', 'search'],
143
+ params: [
144
+ { model: 'name', flag: '--name', type: 'string', description: 'file name substring to search for', required: true },
145
+ ],
146
+ },
147
+ {
148
+ name: 'etscli_meeting_list',
149
+ description: 'list all meetings',
150
+ argv: ['meeting', 'list'],
151
+ params: [],
152
+ },
153
+ {
154
+ name: 'etscli_chat_list',
155
+ description: 'list group chats',
156
+ argv: ['chat', 'list'],
157
+ params: [],
158
+ },
159
+ {
160
+ name: 'etscli_sharing_list',
161
+ description: 'list all sharing links',
162
+ argv: ['sharing', 'list'],
163
+ params: [],
164
+ },
165
+ ];
166
+ /**
167
+ * §7.120:**只读** etscli 子命令的 argv 前缀清单(从注册表派生,单一事实源)。
168
+ *
169
+ * `etscli-shell-guard` 用它做**白名单**:shell 里只允许这几个只读子命令,
170
+ * 其余一律拒绝。派生而不是另抄一份 ⇒ 注册表加一个只读工具,白名单自动跟上;
171
+ * 反过来,写子命令永远不在白名单里(它们压根没注册)。
172
+ */
173
+ export const ETSCLI_READONLY_ARGV = ETSCLI_TOOL_SPECS.map(spec => spec.argv);
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ // Shell quoting
176
+ // ─────────────────────────────────────────────────────────────────────────────
177
+ /**
178
+ * POSIX single-quote escaping。值只可能来自模型的 tool arguments,但仍然
179
+ * 一律走这个函数 —— `ctx.shell` 最终是 `bash -c <command>`(`dsh-bash-local`
180
+ * `:214-220`),所以引号是必需的,不是可选的洁癖。
181
+ *
182
+ * 单引号内除 `'` 外全部字面;`'` 用经典的 `'\''` 闭合-转义-重开。
183
+ * 空串显式编成 `''` 而不是留空(留空会让 flag 吞掉下一个 token)。
184
+ */
185
+ export function shellQuote(value) {
186
+ return `'${value.replace(/'/g, `'\\''`)}'`;
187
+ }
188
+ /** 把一个工具的 args 拼成 `etscli <subcmd...> --flag <quoted> ...`。 */
189
+ export function buildCommand(bin, spec, args) {
190
+ const parts = [shellQuote(bin), ...spec.argv.map(shellQuote)];
191
+ for (const param of spec.params) {
192
+ const raw = args[param.model];
193
+ if (raw === undefined || raw === null)
194
+ continue;
195
+ const asText = param.type === 'integer'
196
+ ? String(raw)
197
+ : String(raw);
198
+ if (asText === '')
199
+ continue;
200
+ parts.push(param.flag, shellQuote(asText));
201
+ }
202
+ return parts.join(' ');
203
+ }
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+ // Output schema
206
+ // ─────────────────────────────────────────────────────────────────────────────
207
+ /**
208
+ * `@deepseek-ai/dsh-tools` 的 JSON Schema 子集受限(`json-schema.js:28-39`):
209
+ * 只有 type / oneOf / properties / required / additionalProperties / items /
210
+ * enum / const 是约束关键字,description / title / default / examples 是注解
211
+ * 关键字。`type` 与 `oneOf` 互斥(`:219-221`)。
212
+ */
213
+ const OUTPUT_SCHEMA = {
214
+ type: 'object',
215
+ additionalProperties: false,
216
+ properties: {
217
+ exitCode: { type: 'integer' },
218
+ signal: { type: 'string' },
219
+ timedOut: { type: 'boolean' },
220
+ stdout: {
221
+ type: 'object',
222
+ additionalProperties: false,
223
+ properties: {
224
+ text: { type: 'string' },
225
+ truncated: { type: 'boolean' },
226
+ },
227
+ },
228
+ stderr: {
229
+ type: 'object',
230
+ additionalProperties: false,
231
+ properties: {
232
+ text: { type: 'string' },
233
+ truncated: { type: 'boolean' },
234
+ },
235
+ },
236
+ },
237
+ };
238
+ /** 喂给模型的文本上限。`etscli file search` 在满库时能吐几百 KB。 */
239
+ const MAX_OUTPUT_CHARS = 8_000;
240
+ function renderResult(value) {
241
+ const out = value.stdout.text ?? '';
242
+ const err = value.stderr.text ?? '';
243
+ const head = value.timedOut
244
+ ? '[etscli] command timed out'
245
+ : `[exit code: ${value.exitCode ?? 'null'}${value.signal !== undefined ? ` (signal ${value.signal})` : ''}]`;
246
+ const body = out.length > MAX_OUTPUT_CHARS
247
+ ? `${out.slice(0, MAX_OUTPUT_CHARS)}\n… [truncated ${out.length - MAX_OUTPUT_CHARS} chars]`
248
+ : out;
249
+ const errPart = err.trim() === '' ? '' : `\n--- stderr ---\n${err.slice(0, 2_000)}`;
250
+ return `${head}\n${body}${errPart}`;
251
+ }
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ // Plugin
254
+ // ─────────────────────────────────────────────────────────────────────────────
255
+ export const name = 'etscli-tools';
256
+ export const inject = ['tools', 'shell'];
257
+ /**
258
+ * etscli 二进制的默认绝对路径(没有配置时的兜底)。
259
+ *
260
+ * Windows 走 `USERPROFILE`,其余走 `HOME`。**单一事实源**:§7.120 的 shell
261
+ * 守卫(`etscli-shell-guard.ts`,独立插件行)也要认绝对路径调用,它从这里
262
+ * import 这个兜底 —— 两处各写一份路径,早晚会长歪。
263
+ */
264
+ export function defaultEtscliBin() {
265
+ return `${(process.env.USERPROFILE ?? process.env.HOME) ?? ''}/.local/bin/etscli`;
266
+ }
267
+ export const Config = Schema.object({
268
+ // §7.102 "one config file": no computed default — apply() fills from
269
+ // config/edge.json (etscli.*) or the ~/.local/bin/etscli default.
270
+ etscliBin: Schema.string(),
271
+ timeoutMs: Schema.natural(),
272
+ });
273
+ export async function apply(ctx, config) {
274
+ // §7.102: fill unset knobs from config/edge.json (etscli.*).
275
+ const e = loadEdgeConfig().etscli;
276
+ const ec = config;
277
+ if (ec.etscliBin === undefined) {
278
+ ec.etscliBin = e?.bin && e.bin.length > 0
279
+ ? e.bin
280
+ : `${(process.env.USERPROFILE ?? process.env.HOME) ?? ''}/.local/bin/etscli`;
281
+ }
282
+ if (ec.timeoutMs === undefined && e?.timeoutMs !== undefined)
283
+ ec.timeoutMs = e.timeoutMs;
284
+ // 代理 trap 防护同 `tools.ts:275-291`:cordis 未 inject 的 service 访问会静默
285
+ // throw "cannot get property X without inject"(§7.92.2),包 try/catch 后给出
286
+ // 可读的失败原因而不是半个插件静默装死。
287
+ let registrar;
288
+ let shell;
289
+ try {
290
+ registrar = ctx.tools;
291
+ shell = ctx.shell;
292
+ }
293
+ catch (err) {
294
+ throw new Error(`[etscli-tools] cannot access ctx.tools / ctx.shell: ${String(err)}`);
295
+ }
296
+ if (registrar === undefined)
297
+ throw new Error('[etscli-tools] ctx.tools is not available');
298
+ if (shell === undefined)
299
+ throw new Error('[etscli-tools] ctx.shell is not available');
300
+ ctx.logger?.info?.(`[etscli-tools] registering ${ETSCLI_TOOL_SPECS.length} read-only tools, bin=${config.etscliBin}`);
301
+ // ── §7.120:逃逸口守卫**不在本插件里** ────────────────────────────────
302
+ //
303
+ // 「bash 里直接调 etscli 写子命令」这条逃逸口的封堵是**独立插件行**
304
+ // (`etscli-shell-guard`,单独挂载)—— 理由:
305
+ //
306
+ // - **覆盖面**:两个部署 profile 原本都**没挂**本插件(http profile 与
307
+ // desktop-safe preset 都没有 `etscli-tools` 行)⇒ 守卫写在��插件里就
308
+ // 等于**哪都没上膛**。§7.118 那次 MEOS 写入事故恰恰发生在**没挂本插件的
309
+ // headless HTTP profile**里 ⇒ 守卫必须能**独立**挂载。
310
+ // - **不新增能力**:独立行只加一道闸,**不**把那 14 个只读 etscli 工具带进
311
+ // 那些 profile(带进去是能力扩张,超出「封堵逃逸口」这件事本身)。
312
+ //
313
+ // 白名单的单一事实源仍是本模块的 `ETSCLI_READONLY_ARGV`(守卫从���里 import,
314
+ // 单向依赖,不成环)。
315
+ for (const spec of ETSCLI_TOOL_SPECS) {
316
+ // 重复注册同名会 throw 并中断整个 apply()(NamedEntries,dsh-tools `lib/index.js:2538`),
317
+ // 留下半注册状态。所以这里**不做部分成功兜底** —— 要么全进,要么整个插件失败、
318
+ // 启动时立刻可见,绝不留下「注册了一半还以为全好了」的静默状态。
319
+ registrar.register({
320
+ name: spec.name,
321
+ description: spec.description,
322
+ parameters: Object.fromEntries(spec.params.map(p => [
323
+ p.model,
324
+ {
325
+ type: p.type,
326
+ ...(p.required ? { required: true } : {}),
327
+ description: p.description,
328
+ },
329
+ ])),
330
+ output: {
331
+ schema: OUTPUT_SCHEMA,
332
+ render: (_args, value) => [
333
+ { type: 'text', text: renderResult(value) },
334
+ ],
335
+ },
336
+ timeoutMs: config.timeoutMs,
337
+ // 契约要求**精确** `true` 才并发(省略/抛异常都退回串行)。14 个全是只读,
338
+ // 无共享可变状态,明确 opt in。
339
+ isConcurrencySafe: () => true,
340
+ async execute(args, exec) {
341
+ const record = args !== null && typeof args === 'object' && !Array.isArray(args)
342
+ ? args
343
+ : {};
344
+ const command = buildCommand(config.etscliBin, spec, record);
345
+ const result = await shell.run(shell.resolve({
346
+ command,
347
+ timeoutMs: config.timeoutMs,
348
+ // 透传调用方的 signal:上游 turn 被取消时,子进程必须跟着被杀,
349
+ // 否则 etscli 会变成孤儿进程继续跑(§7.42 的 async-wrap 教训同源)。
350
+ signal: exec.signal,
351
+ }));
352
+ // 注意:execute 返回**纯 JSON**,不是 ContentBlock —— 投影成模型可读的
353
+ // 文本是 output.render 的活儿(见模块头引的 dsh-tool-web web_fetch)。
354
+ //
355
+ // ⚠️ **null 字段必须省略,不能置 null**:output.schema 的受限关键词里
356
+ // 表达不了 nullable(`signal: {type:'string'}` 遇到 `null` 直接判
357
+ // "must be a string",整次执行被拒 —— §7.98 smoke 实测
358
+ // INVALID_TOOL_OUTPUT)。正常退出时 signal=null、被信号杀死时
359
+ // exitCode=null,两个可空字段都只在非 null 时才进对象。
360
+ const value = {
361
+ timedOut: result.timedOut,
362
+ stdout: { text: result.stdout.text ?? '', truncated: result.stdout.truncated ?? false },
363
+ stderr: { text: result.stderr.text ?? '', truncated: result.stderr.truncated ?? false },
364
+ };
365
+ if (result.exitCode !== null)
366
+ value.exitCode = result.exitCode;
367
+ if (result.signal !== null)
368
+ value.signal = result.signal;
369
+ return value;
370
+ },
371
+ });
372
+ }
373
+ ctx.logger?.info?.(`[etscli-tools] registered: ${ETSCLI_TOOL_SPECS.map(s => s.name).join(', ')}`);
374
+ // 把注册表当前的全量工具名打进 ops log。用途有二:
375
+ // 1. §7.98 的 realm 白名单要靠它 —— `filterToolsField` 对**未点名**的工具
376
+ // fail-open 保留(`realm-skill-bridge.ts:713-717`),不逐个标 `"hidden"`
377
+ // 就等于把 `bash` 一起发给 needle(needle 选中它 = 可执行任意命令)。
378
+ // 凭这份真实清单才能保证不漏标。
379
+ // 2. 排查"needle 为什么选了个奇怪的东西"时的第一手证据。
380
+ try {
381
+ const all = ctx.tools?.schemas?.() ?? [];
382
+ const names = all.map(s => s?.name).filter((n) => typeof n === 'string');
383
+ const ours = new Set(ETSCLI_TOOL_SPECS.map(s => s.name));
384
+ ctx.logger?.info?.(`[etscli-tools] registry total=${names.length} ` +
385
+ `etscli=${names.filter(n => ours.has(n)).length} ` +
386
+ `other=[${names.filter(n => !ours.has(n)).sort().join(', ')}]`);
387
+ }
388
+ catch (err) {
389
+ ctx.logger?.warn?.(`[etscli-tools] failed to dump registry tool list: ${String(err)}`);
390
+ }
391
+ // 本插件只注册只读工具,没有需要注销的监听器 ⇒ 返回空 disposer。
392
+ return () => { };
393
+ }