@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,420 @@
1
+ /**
2
+ * etscli-shell-guard — §7.120:封堵「bash 里直接调 etscli 写子命令」这条逃逸口
3
+ *
4
+ * ## 为什么需要这个模块
5
+ *
6
+ * §7.98 的安全边界是**注册表白名单**:只把 14 个**只读** etscli 子命令注册进
7
+ * `ctx.tools`,写类(`file upload` / `file delete` / `fs rm` / `chat send` /
8
+ * `auth logout` / `migrate *` / `sharing create` / `meeting summary` /
9
+ * `file download` …)一个都不注册 ⇒ 模型在**工具清单**里选不到它们。
10
+ *
11
+ * 但 §7.118 那次实测把这条边界捅穿了:agent 直接
12
+ * `bash: etscli file upload ...`,把本地文件传到了 MEOS 设备的
13
+ * `/备忘/联系方式.txt`。原因(§7.120 查清):
14
+ *
15
+ * 1. 宿主的 **Auto review 闸门只在 `auto` preset 下安装**,本仓 edge preset 用
16
+ * `workspace-write` ⇒ 没有风险分类器;
17
+ * 2. 剩下的**沙箱越界升级是纯文件系统视角** —— `etscli file upload` 改的是
18
+ * **远端 MEOS 设备**(走 etscli 自己的协议),对文件系统沙箱完全不可见 ⇒
19
+ * 不越界 ⇒ 不升级 ⇒ **不触发审批**;
20
+ * 3. 于是 `bash` 这个通用逃逸口成了唯一的真身在侧:白名单管的是「工具清单」,
21
+ * 管不住「模型自己在 shell 里敲什么」。
22
+ *
23
+ * ## 主人裁决(2026-10-31)与机制落点
24
+ *
25
+ * > 用**插件侧守卫**封堵 —— 拦 bash 里 etscli 的写子命令。
26
+ *
27
+ * 裁决的实质是「**插件侧**封堵,不碰宿主启动环境(不改 PATH / 不改启动脚本),
28
+ * 桌面与无头两个 profile 都生效」。机制落点从 `ctx.tools.guard()` 换成
29
+ * `tools/pre-execute` waterfall,理由是三条**可查证**的事实:
30
+ *
31
+ * 1. **`ctx.tools.guard()` 不接受调用方的 scope。**
32
+ * 宿主实现是 `guard(guard) { return this.layers.effect(this.ctx, …) }`
33
+ * (`@deepseek-ai/dsh-tools` `lib/index.js:3017-3022`)—— 注册进的是
34
+ * **ToolRuntime 服务自己那个 ctx** 所在的层。插件侧拿到 `ctx.tools` 时,
35
+ * 拿不到、也改不了那个 ctx ⇒ **无法把守卫绑到边缘 preset 的 scope 上**;
36
+ * 服务在宿主根上下文提供时,那一层就是**全局层** ⇒ 标准模式会话也会被拦,
37
+ * 而标准模式会话的审计链必须**零本项目事件**(§7.140 不变式)。
38
+ * 2. **宿主的插件沙箱 façade 刻意不暴露 `guard`。**
39
+ * `dsh-cordis-host-runner/lib/types/guard.js:581-588` 给插件的 `ctx.tools`
40
+ * 只留 `register` / `schemas` / `get`(注释里点名 `guard` 属于
41
+ * 「identity protection, pre-policy, **monotonic guards**, …」这一串
42
+ * 不许旁路的机制)。桌面 profile 现在给的是真服务(`ctx.tools.view()`
43
+ * 能用 ⇒ boot scan 数出 28 个工具),但这属于**沙箱边界之外的当前状态**,
44
+ * 不是稳定契约。
45
+ * 3. **`tools/pre-execute` 既是宿主点名的扩展点,又是 scope 感知的。**
46
+ * 宿主注释原话(`lib/index.js:3008`):「Register a monotonic guard **after
47
+ * the extensible `tools/pre-execute` waterfall**」。派发时
48
+ * `carrier = scopeTarget(this, exec.agent)`(`lib/index.js:3320-3321`)——
49
+ * 监听器按 scope 链派发,**边缘 preset 的监听器只收到边缘会话的事件**,
50
+ * 标准模式会话的 scope 链上没有本插件的 scope ⇒ 收不到。
51
+ *
52
+ * 而 §7.136 已经把这条路在本仓跑通了:`realm-skill-bridge` 的 Layer 3 realm
53
+ * 闸门就是同一个 waterfall(`realm-skill-bridge.ts:1182`),审计链里有 6 条
54
+ * `tool.realm.denied`(`level: security`)作实证。
55
+ *
56
+ * 拒绝语义与 `guard()` 等价且同样**单调**:waterfall 判 `deny` 时
57
+ * `decision.reason` 直接成为拒绝理由(`lib/index.js:3337-3343`),工具**不派发**,
58
+ * 后续监听器与守卫都放不回来。
59
+ *
60
+ * ## 这条守卫堵的是「逃逸口」,不是「能力」
61
+ *
62
+ * 技能(`ets-file` / `ets-chat` / `ets-auth` …)走的是**宿主 skill 工具**
63
+ * (`exec.name === 'skill'`,参数 `skillName`),**不经过 bash**,因此**不受影响**
64
+ * —— 主人装的那些技能照常可用。本守卫只拦「模型自己在 shell 里敲 etscli」这条路。
65
+ *
66
+ * 诚实记录它的边界:**命令文本匹配可以被绝对路径调用、复制二进制、别名或拼接
67
+ * 绕过** ⇒ 它不是能力层的等价物。真正封死「模型按名字够到那个二进制」仍要靠
68
+ * 摘 PATH(主人本轮裁决不做)。两条是**纵深防御**关系。
69
+ *
70
+ * ## 依赖方向
71
+ *
72
+ * 本模块 import `etscli-tools.ts` 的 `ETSCLI_READONLY_ARGV`(白名单的**单一
73
+ * 事实源** = 从只读工具注册表派生的那份)—— **单向**依赖:`etscli-tools.ts`
74
+ * 不 import 本模块(否则成环:环在这里恰好不炸,那是求值顺序的运气)。
75
+ *
76
+ * ## 为什么是**独立插件行**,而不是写在 etscli-tools 里
77
+ *
78
+ * §7.118 那次 MEOS 写入事故发生在**headless HTTP profile**里,而那个 profile
79
+ * (以及 desktop-safe preset)**本来就没挂** `etscli-tools` ⇒ 守卫写在它里面
80
+ * 就等于**哪个部署都没上膛**(「代码写了、测试过了、闸门没装」是本仓最怕的
81
+ * 那种状态)。独立成行后可以**按需挂载**:
82
+ *
83
+ * - `profiles/dsh-edge-agent/cordis.patch.yml`(headless HTTP profile ——
84
+ * 事故发生的那个);
85
+ * - `presets/edge.desktop-safe.patch.yml`(桌面 preset);
86
+ * - `presets/edge.patch.yml`(完整 edge preset)。
87
+ *
88
+ * 独立行只**加一道闸**,**不**把那 14 个只读 etscli 工具带进那些 profile ——
89
+ * 带进去是能力扩张,超出「封堵逃逸口」这件事本身。
90
+ *
91
+ * @module dsh-edge-agent/etscli-shell-guard
92
+ */
93
+ import Schema from '@deepseek-ai/schemastery';
94
+ import { loadEdgeConfig } from './edge-config.js';
95
+ import { defaultEtscliBin, ETSCLI_READONLY_ARGV } from './etscli-tools.js';
96
+ /** 插件名(cordis patch 行的 id 与本名一致)。 */
97
+ export const name = 'etscli-shell-guard';
98
+ /**
99
+ * 没有服务依赖(§7.169):只用 `ctx.on`(cordis 的框架动词)。
100
+ *
101
+ * **刻意不声明 `auditLog`**:声明成硬依赖后,本行要等 auditLog 服务就绪
102
+ * 才 apply —— 那条服务一旦缺席,**闸门整个不装上**(逃逸口静默敞开)。
103
+ * 闸门必须先于任何证据通道就位。审计服务改为**延迟注入**捕获
104
+ * (见 apply() 内注释):审计是证据通道,不是闸门的依赖。
105
+ */
106
+ export const inject = [];
107
+ /** 能执行任意命令的工具名(小写比较)。 */
108
+ export const SHELL_TOOL_NAMES = ['bash', 'pwsh'];
109
+ /** 命令里出现的 shell 分隔符 / 重定向符 —— 遇到它就算「上一条命令结束」。 */
110
+ const SEPARATORS = new Set([';', '|', '&', '>', '<', '`', '(', ')']);
111
+ /** 允许:没命中任何 etscli 调用。 */
112
+ const ALLOW = { denied: false };
113
+ /**
114
+ * 极简 shell 分词。
115
+ *
116
+ * 够用就好:这里的目的是**找出命令文本里有没有 etscli 调用**,不是做一个
117
+ * 完整的 shell 解析器。处理引号、转义、以及 `;` `|` `&` `>` `<` 这些分隔符,
118
+ * 好让 `etscli` 后面只取到属于它自己的那几个 token。
119
+ */
120
+ export function tokenizeShell(command) {
121
+ const tokens = [];
122
+ let cur = '';
123
+ let has = false;
124
+ let quote = null;
125
+ const flush = () => {
126
+ if (has) {
127
+ tokens.push(cur);
128
+ cur = '';
129
+ has = false;
130
+ }
131
+ };
132
+ for (let i = 0; i < command.length; i++) {
133
+ const ch = command[i];
134
+ if (quote !== null) {
135
+ if (ch === '\\' && quote === '"' && i + 1 < command.length) {
136
+ cur += command[++i];
137
+ has = true;
138
+ continue;
139
+ }
140
+ if (ch === quote) {
141
+ quote = null;
142
+ has = true;
143
+ continue;
144
+ }
145
+ cur += ch;
146
+ has = true;
147
+ continue;
148
+ }
149
+ if (ch === '"' || ch === "'") {
150
+ quote = ch;
151
+ has = true;
152
+ continue;
153
+ }
154
+ if (ch === '\\' && i + 1 < command.length) {
155
+ cur += command[++i];
156
+ has = true;
157
+ continue;
158
+ }
159
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
160
+ flush();
161
+ continue;
162
+ }
163
+ if (SEPARATORS.has(ch)) {
164
+ flush();
165
+ tokens.push(ch);
166
+ continue;
167
+ }
168
+ cur += ch;
169
+ has = true;
170
+ }
171
+ flush();
172
+ return tokens;
173
+ }
174
+ /** 取路径的末段(`/usr/local/bin/etscli` → `etscli`)。 */
175
+ function baseName(token) {
176
+ const cut = Math.max(token.lastIndexOf('/'), token.lastIndexOf('\\'));
177
+ return cut < 0 ? token : token.slice(cut + 1);
178
+ }
179
+ /**
180
+ * 这串 argv 是不是白名单里的只读子命令?
181
+ *
182
+ * 判据用**前缀匹配**:白名单项 `['fs','ls']` 匹配 `fs ls --path /tmp`。
183
+ * 精确到 token,不会把 `fs lsx` 误判成 `fs ls`。
184
+ */
185
+ export function isReadOnlyEtscliArgv(argv, allowlist) {
186
+ return allowlist.some(prefix => prefix.every((token, index) => argv[index] === token));
187
+ }
188
+ /**
189
+ * 判定一条 shell 命令里有没有「不该出现的 etscli 调用」。
190
+ *
191
+ * 规则:**只放行白名单里那 14 个只读子命令**,其余一律拒绝。
192
+ *
193
+ * 为什么用白名单而不是黑名单(把 `file upload` / `fs rm` 这些逐个列出来禁):
194
+ * 黑名单只能挡住**已经想到的**写子命令 —— etscli 加一个新写命令,黑名单就漏一个。
195
+ * 白名单的漏法是**多挡**(新加的只读命令也会被挡),而白名单那份清单就在
196
+ * `etscli-tools.ts` 里(注册什么就放行什么),两者天然同步。
197
+ *
198
+ * @param command - shell 命令文本
199
+ * @param options.allowlist - 只读子命令白名单(argv 前缀)
200
+ * @param options.bin - 配置里的 etscli 二进制路径(用于识别绝对路径调用)
201
+ */
202
+ export function evaluateEtscliShellCommand(command, options) {
203
+ if (typeof command !== 'string' || command.length === 0)
204
+ return ALLOW;
205
+ const binBase = options.bin ? baseName(options.bin) : 'etscli';
206
+ const tokens = tokenizeShell(command);
207
+ for (let i = 0; i < tokens.length; i++) {
208
+ const token = tokens[i];
209
+ if (baseName(token) !== binBase)
210
+ continue;
211
+ // etscli 后面属于它的 argv:遇到分隔符就停。
212
+ const argv = [];
213
+ for (let j = i + 1; j < tokens.length; j++) {
214
+ const next = tokens[j];
215
+ if (SEPARATORS.has(next))
216
+ break;
217
+ argv.push(next);
218
+ }
219
+ // 子命令部分 = 前导的非 flag token(最多 3 段,白名单最长的是
220
+ // `system alarm list`)。**刻意不把参数值算进去** —— 那些是本地路径 /
221
+ // 文件名,不该出现在拒绝理由和审计行里。
222
+ const sub = [];
223
+ for (const arg of argv) {
224
+ if (arg.startsWith('-')) {
225
+ // flag 之前的都是全局 flag,跳过;flag 之后的都是参数,停。
226
+ if (sub.length > 0)
227
+ break;
228
+ continue;
229
+ }
230
+ sub.push(arg);
231
+ if (sub.length >= 3)
232
+ break;
233
+ }
234
+ if (isReadOnlyEtscliArgv(sub, options.allowlist))
235
+ continue;
236
+ // 给模型看的标签只取**前两段命令词**(`file upload` / `fs rm` / `chat send`…),
237
+ // 位置参数(本地路径 / 文件名)一律不进标签 —— 拒绝理由会回到模型的上下文里,
238
+ // 审计行也会落盘,不该带着本地路径。
239
+ const label = sub
240
+ .filter(token => !token.includes('/') && !token.includes('\\'))
241
+ .slice(0, 2)
242
+ .join(' ');
243
+ const shown = label.length > 0 ? label : '(未指定子命令)';
244
+ const reason = `etscli-shell-guard: \`etscli ${shown}\` 不是只读子命令,已拒绝执行。` +
245
+ `shell 里只允许这 ${options.allowlist.length} 个只读子命令:` +
246
+ `${options.allowlist.map(p => p.join(' ')).join('、')};` +
247
+ `需要别的 etscli 能力请走对应的 skill 或已注册的 etscli_* 工具。`;
248
+ return { denied: true, subcommand: shown, reason };
249
+ }
250
+ return ALLOW;
251
+ }
252
+ /** 收集参数对象里所有字符串值(深度受限)。 */
253
+ function collectStrings(value, out, depth) {
254
+ if (depth > 6 || out.length >= 32)
255
+ return;
256
+ if (typeof value === 'string') {
257
+ if (value.length > 0)
258
+ out.push(value);
259
+ return;
260
+ }
261
+ if (Array.isArray(value)) {
262
+ for (const item of value)
263
+ collectStrings(item, out, depth + 1);
264
+ return;
265
+ }
266
+ if (value !== null && typeof value === 'object') {
267
+ for (const item of Object.values(value)) {
268
+ collectStrings(item, out, depth + 1);
269
+ }
270
+ }
271
+ }
272
+ /**
273
+ * 判定一次工具执行里有没有「不该出现的 etscli 调用」。
274
+ *
275
+ * 只看 **shell 类工具**(`bash` / `pwsh`)—— 技能走的是 `skill` 工具(参数
276
+ * `skillName`),不受影响;`etscli_*` 白名单工具走绝对路径直接调二进制,
277
+ * 也不经过 shell 工具。
278
+ *
279
+ * 参数值**不按参数名取**:把 exec 的参数对象里所有字符串值都过一遍。这样
280
+ * 不用依赖宿主 bash 工具的参数名(`command` / `cmd` / …),也不会因为参数名
281
+ * 变了就漏检。
282
+ */
283
+ export function evaluateEtscliToolCall(exec, options) {
284
+ const typed = (exec ?? {});
285
+ const name = typeof typed.name === 'string' ? typed.name.toLowerCase() : '';
286
+ if (!SHELL_TOOL_NAMES.includes(name))
287
+ return ALLOW;
288
+ const args = typed.arguments ?? typed.args ?? typed.input;
289
+ if (args === null || typeof args !== 'object')
290
+ return ALLOW;
291
+ const texts = [];
292
+ collectStrings(args, texts, 0);
293
+ for (const text of texts) {
294
+ const verdict = evaluateEtscliShellCommand(text, {
295
+ allowlist: options.allowlist,
296
+ bin: options.bin,
297
+ });
298
+ if (verdict.denied)
299
+ return verdict;
300
+ }
301
+ return ALLOW;
302
+ }
303
+ /**
304
+ * 把守卫注册到宿主的 `tools/pre-execute` waterfall。
305
+ *
306
+ * 契约(`@deepseek-ai/dsh-tools` `lib/index.js:3319-3343`):
307
+ *
308
+ * - 监听器返回 `{kind:'deny', reason}` ⇒ 这次执行**不派发**,`reason` 成为
309
+ * 拒绝理由(工具结果里的 `Error: <reason>`),后续监听器/守卫放不回来;
310
+ * - 放行时必须 `return next()` —— 直接 `allow` 会把后面的链条截断。
311
+ *
312
+ * 本监听器从**本插件自己的 ctx** 注册,而本插件挂在边缘 preset 的 scope 里
313
+ * ⇒ 宿主按 scope 链派发(`scopeTarget(this, exec.agent)`)⇒ 标准模式会话
314
+ * 收不到本事件 ⇒ 守卫不会伸到标准模式(§7.140 的零事件不变式不受影响)。
315
+ *
316
+ * @returns 宿主返回的**精确 disposer**(用于有序拆卸 / HMR 清理)
317
+ */
318
+ export function registerEtscliShellGuard(ctx, options) {
319
+ const on = ctx.on;
320
+ if (typeof on !== 'function') {
321
+ // 缺了闸门就等于逃逸口敞开 —— 按本仓「宁可在启动时就说清楚缺了谁」的纪律,
322
+ // 直接拒绝启动,而不是让每一道闸在运行期静默失效。
323
+ throw new Error('[etscli-shell-guard] ctx.on is not available: the etscli write-subcommand ' +
324
+ 'escape hatch would be open silently (tools/pre-execute could not be observed). Refusing to start.');
325
+ }
326
+ const disposer = on.call(ctx, 'tools/pre-execute', async (exec, next) => {
327
+ const verdict = evaluateEtscliToolCall(exec, {
328
+ allowlist: options.allowlist,
329
+ bin: options.bin,
330
+ });
331
+ if (!verdict.denied)
332
+ return next();
333
+ const typed = (exec ?? {});
334
+ const tool = typeof typed.name === 'string' ? typed.name.toLowerCase() : 'unknown';
335
+ const reason = verdict.reason ?? 'etscli-shell-guard: 拒绝执行';
336
+ options.onDeny?.({
337
+ tool,
338
+ subcommand: verdict.subcommand ?? '(未指定子命令)',
339
+ reason,
340
+ });
341
+ return { kind: 'deny', reason };
342
+ });
343
+ return typeof disposer === 'function' ? disposer : () => { };
344
+ }
345
+ export const Config = Schema.object({
346
+ etscliBin: Schema.string(),
347
+ });
348
+ /**
349
+ * 装上这道闸。
350
+ *
351
+ * 装的顺序与两件记录:
352
+ *
353
+ * 1. **启动行**(`armed`):只记拒绝行的话,"守卫没装上" 和 "守卫装上了但这次
354
+ * 没人触发" 在日志里长得一模一样 ⇒ 启动时把**白名单规模**与**机制落点**
355
+ * 写出来。不记放行行(每次 bash 调用都记会淹掉真正的拒绝行;realm 闸门那层
356
+ * 已经在逐次记 tool allow/deny)。
357
+ * 2. **拒绝行**:ops log `warn` + 审计行 `etscli.shell.denied`
358
+ * (`level: security`;载荷**不含命令原文** —— 里面可能有本地路径与文件名)。
359
+ */
360
+ export async function apply(ctx, config) {
361
+ const configured = loadEdgeConfig().etscli;
362
+ const bin = (config.etscliBin && config.etscliBin.length > 0)
363
+ ? config.etscliBin
364
+ : (configured?.bin && configured.bin.length > 0 ? configured.bin : defaultEtscliBin());
365
+ // 审计服务用**延迟注入**捕获(§7.169)。
366
+ //
367
+ // 为什么不用 `ctx.get('auditLog')`:宿主 cordis 的 `get(name)` 走
368
+ // `ctx[isolate][name]` —— 只有**声明在 inject 里**的名字才进 isolate 表。
369
+ // 本插件刻意不声明 auditLog(见上方 inject 的说明),所以
370
+ // `ctx.get('auditLog')` 恒为 undefined ⇒ 拒绝路径的审计行**从未落过地**
371
+ // (实测 2026-10-31:探针打印 `ctx.get(auditLog) type=undefined`)。
372
+ //
373
+ // 延迟注入(`ctx.inject(['auditLog'], cb)`)另起一根 fiber,它自己的
374
+ // isolate 表里有 auditLog ⇒ 回调里拿得到服务。拒绝真发生时,审计行
375
+ // 才需要它,那时服务早已就位。
376
+ let auditLog;
377
+ ctx.inject(['auditLog'], (raw) => {
378
+ auditLog = raw?.auditLog;
379
+ });
380
+ const disposer = registerEtscliShellGuard(ctx, {
381
+ // 白名单的单一事实源:从只读工具注册表派生的那份(本模块 import 过来,
382
+ // 单向依赖)。守卫不自己造第二份清单。
383
+ allowlist: ETSCLI_READONLY_ARGV,
384
+ bin,
385
+ onDeny: denial => {
386
+ ctx.logger?.warn?.(`[etscli-shell-guard] denied tool=${denial.tool} subcommand=${denial.subcommand}`);
387
+ void auditLog?.log?.({
388
+ level: 'security',
389
+ kind: 'etscli.shell.denied',
390
+ source: 'etscli-shell-guard',
391
+ payload: {
392
+ tool: denial.tool,
393
+ subcommand: denial.subcommand,
394
+ // 刻意**不记命令原文**:里面可能有本地路径与文件名。
395
+ reason: denial.reason.slice(0, 300),
396
+ },
397
+ })?.catch?.(() => { });
398
+ },
399
+ });
400
+ // 「已上膛」的凭据必须落在**已经就位**的通道上(§7.169)。
401
+ //
402
+ // 为什么不能直接在 apply() 末尾 `ctx.logger.info(...)`:ops log 的落盘
403
+ // 导出器由 `file-logger` 插件注册,而**插件的 apply 先后由模块解析速度
404
+ // 决定**(实测:守卫比 file-logger 先 apply)⇒ apply 那一刻 logger 还没
405
+ // 有导出器 ⇒ 那一行**静默丢失**(实测 t0 丢、t300 起才落盘)。
406
+ //
407
+ // 办法:延迟注入 `fileLogger`。宿主 cordis 只在 providing fiber **变为
408
+ // ACTIVE 之后**才 notify 依赖方(`_updateState`),而导出器正是在那之前
409
+ // 注册好的 ⇒ 回调触发时通道必定可用 ⇒ 这一行必定落盘,且**与加载顺序
410
+ // 无关**。服务已经就位时回调立即触发(桌面 preset 就是这种),还没就位
411
+ // 就等它(headless profile 是这种)。
412
+ //
413
+ // 刻意**不在 apply 时先打一次**:那一刻通道可能还没有导出器,那一行会
414
+ // 悄悄丢掉,还会让后面那次被"只记一次"的标志吞掉 ⇒ 反而永远看不到。
415
+ ctx.inject(['fileLogger'], () => {
416
+ ctx.logger?.info?.(`[etscli-shell-guard] armed on tools/pre-execute, read-only allowlist=${ETSCLI_READONLY_ARGV.length} ` +
417
+ `bin=${bin} shellTools=${SHELL_TOOL_NAMES.join('/')}`);
418
+ });
419
+ return disposer;
420
+ }
@@ -0,0 +1,112 @@
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 type { Context } from '@deepseek-ai/cordis';
52
+ import Schema from '@deepseek-ai/schemastery';
53
+ interface EtscliParamSpec {
54
+ /** 暴露给模型的名字(与 `etscli-tools-test.py` 的 `_TOOL_SPEC` 保持一致)。 */
55
+ readonly model: string;
56
+ /** 真实 CLI flag。⚠️ 与模型侧参数名**不是**一一对应,见下表注释。 */
57
+ readonly flag: string;
58
+ readonly type: 'string' | 'integer';
59
+ readonly description: string;
60
+ readonly required?: true;
61
+ }
62
+ interface EtscliToolSpec {
63
+ readonly name: string;
64
+ readonly description: string;
65
+ /**
66
+ * 固定子命令 argv 前缀。**永不来自模型或用户输入** —— 这是命令注入的
67
+ * 第一道也是最重要的一道边界:模型只能选「用哪个只读子命令」,不能改写它。
68
+ */
69
+ readonly argv: readonly string[];
70
+ readonly params: readonly EtscliParamSpec[];
71
+ /** argv 之后的分隔符(etscli 的全局 flag 位置敏感时用)。 */
72
+ readonly trailing?: boolean;
73
+ }
74
+ /**
75
+ * §7.120:**只读** etscli 子命令的 argv 前缀清单(从注册表派生,单一事实源)。
76
+ *
77
+ * `etscli-shell-guard` 用它做**白名单**:shell 里只允许这几个只读子命令,
78
+ * 其余一律拒绝。派生而不是另抄一份 ⇒ 注册表加一个只读工具,白名单自动跟上;
79
+ * 反过来,写子命令永远不在白名单里(它们压根没注册)。
80
+ */
81
+ export declare const ETSCLI_READONLY_ARGV: readonly (readonly string[])[];
82
+ /**
83
+ * POSIX single-quote escaping。值只可能来自模型的 tool arguments,但仍然
84
+ * 一律走这个函数 —— `ctx.shell` 最终是 `bash -c <command>`(`dsh-bash-local`
85
+ * `:214-220`),所以引号是必需的,不是可选的洁癖。
86
+ *
87
+ * 单引号内除 `'` 外全部字面;`'` 用经典的 `'\''` 闭合-转义-重开。
88
+ * 空串显式编成 `''` 而不是留空(留空会让 flag 吞掉下一个 token)。
89
+ */
90
+ export declare function shellQuote(value: string): string;
91
+ /** 把一个工具的 args 拼成 `etscli <subcmd...> --flag <quoted> ...`。 */
92
+ export declare function buildCommand(bin: string, spec: EtscliToolSpec, args: Record<string, unknown>): string;
93
+ export declare const name = "etscli-tools";
94
+ export declare const inject: string[];
95
+ /**
96
+ * etscli 二进制的默认绝对路径(没有配置时的兜底)。
97
+ *
98
+ * Windows 走 `USERPROFILE`,其余走 `HOME`。**单一事实源**:§7.120 的 shell
99
+ * 守卫(`etscli-shell-guard.ts`,独立插件行)也要认绝对路径调用,它从这里
100
+ * import 这个兜底 —— 两处各写一份路径,早晚会长歪。
101
+ */
102
+ export declare function defaultEtscliBin(): string;
103
+ export interface Config {
104
+ /** etscli 可执行文件绝对路径。 */
105
+ etscliBin: string;
106
+ /** 单次调用超时(交给 `ctx.shell` 钳位,不是我们自己掐表)。 */
107
+ timeoutMs: number;
108
+ }
109
+ export declare const Config: Schema<Config>;
110
+ export declare function apply(ctx: Context, config: Config): Promise<() => void>;
111
+ export {};
112
+ //# sourceMappingURL=etscli-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"etscli-tools.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/etscli-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AA2C7C,UAAU,eAAe;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAA;CACzB;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;IAC3C,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAoHD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EACrB,CAAA;AAM1C;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAYR;AAgED,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAElC,eAAO,MAAM,MAAM,UAAqB,CAAA;AAExC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,MAAM,WAAW,MAAM;IACrB,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAKhC,CAAA;AAEF,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAoI7E"}