@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,37 @@
1
+ import { createDataActions } from './bot-data-actions.mjs'
2
+
3
+ /** The two data doors share a Host-selected Bot; model arguments never select one. */
4
+ export function createProductDataAccess({ engine, authority, directory, lifecycle }) {
5
+ const actions = createDataActions({ engine })
6
+ /**
7
+ * The one Bot door, for a subject the Host already resolved.
8
+ *
9
+ * The calling Bot comes from the live subject's own committed grant and
10
+ * configuration — never from an argument, a path, an endpoint or another
11
+ * Bot's id. A Bot without the `tool/data` grant, or one whose configuration
12
+ * is not committed yet, is refused HERE with the Host's own reason instead of
13
+ * a vague "unknown subject".
14
+ */
15
+ const botIdForSubject = subject => {
16
+ authority.assertTool(subject, 'tool/data')
17
+ const id = authority.describe(subject).botId
18
+ if (!lifecycle.committedBot(id)) throw Object.assign(new Error('当前 Bot 尚未取得已保存的配置。'), { code: 'INVALID_DATA_OWNER' })
19
+ return id
20
+ }
21
+ return Object.freeze({
22
+ ownerAction(name, payload) {
23
+ if (!Object.hasOwn(actions, name)) throw Object.assign(new Error('未知数据操作。'), { code: 'INVALID_DATA_ACTION' })
24
+ if (!lifecycle.committedBot(payload?.botId)) throw Object.assign(new Error('请选择当前团队中已保存的 Bot。'), { code: 'INVALID_DATA_OWNER' })
25
+ return actions[name](payload)
26
+ },
27
+ /**
28
+ * The SDK door: the Bot is derived from the live session's real Agent
29
+ * object. A matching string id never resolves one, so a model-supplied
30
+ * botId or agent identity cannot reach the database.
31
+ */
32
+ botIdFor(exec) { return botIdForSubject(directory.forAgent(exec?.agent)) },
33
+ botIdForSubject,
34
+ /** The native door: same gate, then the engine for exactly that Bot. */
35
+ forSubject(subject, { signal } = {}) { return engine.forBot(botIdForSubject(subject), { signal }) },
36
+ })
37
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * The ONE description of the five Bot data tools.
3
+ *
4
+ * The SDK registration (bot-data-tools.mjs) and the native MCP handler factory
5
+ * (native-data-tools.mjs) both read this module, so a tool name, a description
6
+ * or an argument projection cannot drift between the two doors: what the model
7
+ * is told and what the Bot is actually called with are the same facts.
8
+ *
9
+ * Authority stays outside this file. `invokeDataTool(bot, name, args)` only
10
+ * projects the business keys an entry declares and calls the existing Bot data
11
+ * service exactly once; the Bot object itself is chosen by the Host
12
+ * (data-product-access.mjs) from committed state, never from an argument. A
13
+ * caller that tries to smuggle `botId`, a path, an endpoint or an operation id
14
+ * through the arguments is refused by `parseDataToolArgs` before any service is
15
+ * reached, and its extra keys would be dropped by the projection anyway.
16
+ *
17
+ * Two schemas describe the same contract on purpose:
18
+ * - `parameters` is the dsh-tools author map the installed SDK compiles, where
19
+ * `data_show.chart` stays the SDK's own `json` node (its narrower validator
20
+ * rejects the nested form);
21
+ * - `inputSchema` is a real MCP JSON Schema object, where `data_show.chart` is
22
+ * a genuinely nested, bounded object built from the fields the chart
23
+ * compiler in bot-data-chart.mjs actually supports. No fake string/JSON type
24
+ * is offered to a native client, and nothing is promised that the compiler
25
+ * would refuse (scatter, histogram, pie, heatmap, median are not offered).
26
+ */
27
+
28
+ import { DATA_TOOL_NAMES } from './bot-data-surface.mjs'
29
+
30
+ /** JSON argument bytes one data call may carry; the same bound the native tools use. */
31
+ const MAX_ARGS_BYTES = 128_000
32
+
33
+ /** Chart vocabulary the compiler in bot-data-chart.mjs really supports. */
34
+ const CHART_MARKS = Object.freeze(['bar', 'line', 'area'])
35
+ const CHART_AGGS = Object.freeze(['count', 'sum', 'avg', 'min', 'max'])
36
+ const CHART_SORTS = Object.freeze(['x', '-x', 'y', '-y'])
37
+ const CHART_TIME_UNITS = Object.freeze(['year', 'quarter', 'month', 'week', 'day', 'hour'])
38
+
39
+ /**
40
+ * The chart contract as a nested MCP object schema. `type` and `x` are required
41
+ * because the compiler refuses a spec without them; everything else is optional
42
+ * exactly where the compiler allows it to be absent (a missing `y` means the
43
+ * mark counts rows, a missing `agg` defaults to sum/count). No field here is
44
+ * nullable, because the compiler treats null as an invalid spec, not as a default.
45
+ */
46
+ const CHART_INPUT_SCHEMA = Object.freeze({
47
+ type: 'object',
48
+ properties: Object.freeze({
49
+ type: Object.freeze({
50
+ type: 'string', enum: CHART_MARKS,
51
+ description: 'Mark to draw: bar, line or area. scatter, histogram, pie and heatmap are refused rather than faked.',
52
+ }),
53
+ x: Object.freeze({ type: 'string', description: 'Column to group by. It must already exist in the table or query.' }),
54
+ y: Object.freeze({ type: 'string', description: 'Column to aggregate. Omit it and the mark counts rows.' }),
55
+ color: Object.freeze({ type: 'string', description: 'Optional column that splits the marks into series.' }),
56
+ agg: Object.freeze({
57
+ type: 'string', enum: CHART_AGGS,
58
+ description: 'Aggregation applied to y: count, sum, avg, min or max. Default: sum when y is set, otherwise count. median is refused.',
59
+ }),
60
+ sort: Object.freeze({ type: 'string', enum: CHART_SORTS, description: 'Order of the marks: x, -x, y or -y. Default x ascending.' }),
61
+ timeUnit: Object.freeze({
62
+ type: 'string', enum: CHART_TIME_UNITS,
63
+ description: 'Bucket x as time: year, quarter, month, week, day or hour.',
64
+ }),
65
+ limit: Object.freeze({
66
+ type: 'integer',
67
+ description: 'Marks kept for the card. The compiler clamps it to at least 1 and at most 1000. Default 20.',
68
+ }),
69
+ stack: Object.freeze({ type: 'boolean', description: 'Stack the series of one mark. Default false.' }),
70
+ }),
71
+ required: Object.freeze(['type', 'x']),
72
+ additionalProperties: false,
73
+ })
74
+
75
+ /** One dsh-tools parameter spec projected to a real JSON Schema property. */
76
+ const jsonSchemaProperty = spec => {
77
+ if (spec.type === 'json') throw new TypeError(`a data tool parameter must not offer the SDK json node to MCP: ${spec.description}`)
78
+ const schema = { type: spec.type }
79
+ if (spec.enum) schema.enum = spec.enum
80
+ schema.description = spec.description
81
+ return Object.freeze(schema)
82
+ }
83
+
84
+ /**
85
+ * The MCP input schema for one tool: the same properties, the same enums, the
86
+ * same required set as the SDK map, with `additionalProperties: false` so a
87
+ * native client is told the truth — there is no `botId`, no path and no
88
+ * authority key to pass.
89
+ */
90
+ const inputSchemaFor = parameters => {
91
+ const properties = {}
92
+ for (const [key, spec] of Object.entries(parameters)) {
93
+ // data_show.chart is the one place the SDK map is a `json` node; MCP gets
94
+ // the real nested object the compiler accepts.
95
+ properties[key] = key === 'chart' && spec.type === 'json' ? CHART_INPUT_SCHEMA : jsonSchemaProperty(spec)
96
+ }
97
+ const required = Object.entries(parameters).filter(([, spec]) => spec.required === true).map(([key]) => key)
98
+ return Object.freeze({
99
+ type: 'object',
100
+ properties: Object.freeze(properties),
101
+ required: Object.freeze(required),
102
+ additionalProperties: false,
103
+ })
104
+ }
105
+
106
+ const defineEntry = ({ name, description, parameters, method, project }) => Object.freeze({
107
+ name,
108
+ description,
109
+ parameters: Object.freeze(parameters),
110
+ inputSchema: inputSchemaFor(parameters),
111
+ capability: 'tool/data',
112
+ method,
113
+ project,
114
+ })
115
+
116
+ const ENTRIES = Object.freeze([
117
+ defineEntry({
118
+ name: 'data_load',
119
+ description: [
120
+ 'Load a dataset into a named table in YOUR OWN Bot database, then query it with data_sql and show or export it.',
121
+ 'Data comes only from your private library (from:"library", a relative path) or bounded inline text (from:"content");',
122
+ 'there are no OS paths, URLs or database connections. Replaces a same-named table unless replace:false (then a suffix is added).',
123
+ 'Returns each table\'s columns, types and row count.',
124
+ ].join(' '),
125
+ method: 'load',
126
+ parameters: {
127
+ from: { type: 'string', enum: ['library', 'content'], required: true, description: 'Where the data comes from.' },
128
+ path: { type: 'string', description: 'Library-relative path (from:"library").' },
129
+ content: { type: 'string', description: 'Inline CSV/TSV/JSON text (from:"content"), bounded.' },
130
+ format: { type: 'string', enum: ['csv', 'tsv', 'json'], description: 'Default: from the path, else csv.' },
131
+ name: { type: 'string', description: 'Table name. Default: from the path or "dataset".' },
132
+ replace: { type: 'boolean', description: 'Default true.' },
133
+ delimiter: { type: 'string', description: 'CSV delimiter. Default ",".' },
134
+ header: { type: 'boolean', description: 'Treat the first row as column names. Default true.' },
135
+ },
136
+ project: args => ({
137
+ from: args.from, path: args.path, content: args.content, format: args.format,
138
+ name: args.name, replace: args.replace, delimiter: args.delimiter, header: args.header,
139
+ }),
140
+ }),
141
+ defineEntry({
142
+ name: 'data_describe',
143
+ description: [
144
+ 'Inspect before you write SQL. With no argument it lists your loaded tables and saved cards (metadata only).',
145
+ 'With a table name it returns columns, types, null-free samples and the exact row count.',
146
+ 'With a SELECT it returns the query\'s columns. Use this to learn column names before data_sql or data_show.',
147
+ ].join(' '),
148
+ method: 'describe',
149
+ parameters: {
150
+ target: { type: 'string', description: 'A table name or a SELECT. Omit to list tables and cards.' },
151
+ },
152
+ // The existing service takes the target itself, not a wrapper object.
153
+ project: args => args.target,
154
+ }),
155
+ defineEntry({
156
+ name: 'data_sql',
157
+ description: [
158
+ 'Run one SQLite statement against your own database. SELECT-like statements return bounded rows and a truncated flag;',
159
+ 'CREATE [OR REPLACE] TABLE|VIEW … AS … is allowed to build a derived table. Everything else (INSERT/UPDATE/DELETE/DROP/',
160
+ 'ATTACH/PRAGMA/…) is refused, and so are multiple statements. Pass name to keep a SELECT as a reusable result table.',
161
+ ].join(' '),
162
+ method: 'sql',
163
+ parameters: {
164
+ sql: { type: 'string', required: true, description: 'One statement.' },
165
+ limit: { type: 'integer', description: 'Rows to return. Default 100, max 1000.' },
166
+ name: { type: 'string', description: 'Keep this SELECT as a named result table for later show/export.' },
167
+ },
168
+ project: args => ({ sql: args.sql, limit: args.limit, name: args.name }),
169
+ }),
170
+ defineEntry({
171
+ name: 'data_show',
172
+ description: [
173
+ 'Render a table or a chart from your own data. Give a table name or a SELECT, and kind "table" or "chart".',
174
+ 'Give cardId instead to re-open a card you already saved: the chart is re-rendered from its saved spec and the table',
175
+ 'from its saved result, so nothing is invented. A chart needs chart:{type:"bar"|"line"|"area", x, y?, agg?, color?, sort?,',
176
+ 'limit?, timeUnit?}; the server compiles it to SQL and the panel draws only the declared data. Returns a declarative',
177
+ 'result the panel renders — never executable HTML/JS.',
178
+ ].join(' '),
179
+ method: 'show',
180
+ parameters: {
181
+ table: { type: 'string', description: 'A loaded or result table name.' },
182
+ sql: { type: 'string', description: 'A SELECT. Use table or sql, not both.' },
183
+ kind: { type: 'string', enum: ['table', 'chart'], required: true, description: 'Table or chart.' },
184
+ title: { type: 'string', description: 'Card title.' },
185
+ chart: { type: 'json', description: 'Chart spec for kind:"chart".' },
186
+ limit: { type: 'integer', description: 'Table rows kept for the card.' },
187
+ cardId: { type: 'string', description: 'Re-open a saved card by its id (from data_show or data_describe).' },
188
+ },
189
+ project: args => ({ table: args.table, sql: args.sql, kind: args.kind, title: args.title, chart: args.chart, limit: args.limit, cardId: args.cardId }),
190
+ }),
191
+ defineEntry({
192
+ name: 'data_export',
193
+ description: [
194
+ 'Export a table, a saved card (cardId) or a SELECT to CSV in your own library under exports/.',
195
+ 'The CSV is neutralised against spreadsheet formula injection; unknown values are never invented.',
196
+ 'Returns the library path, byte count, rowCount (the rows actually written) and sourceRowCount (the source total),',
197
+ 'plus a truncated flag. If the library write fails the export FAILS with output_too_large or write_refused and no path.',
198
+ ].join(' '),
199
+ method: 'exportCsv',
200
+ parameters: {
201
+ table: { type: 'string', description: 'A table name.' },
202
+ cardId: { type: 'string', description: 'A saved card id from data_show.' },
203
+ sql: { type: 'string', description: 'A SELECT.' },
204
+ filename: { type: 'string', description: 'Export file stem. Default "export".' },
205
+ },
206
+ project: args => ({ table: args.table, cardId: args.cardId, sql: args.sql, filename: args.filename }),
207
+ }),
208
+ ])
209
+
210
+ // The data surface names the five tools; a contract that stops matching it is a
211
+ // bug, not a new tool, so it fails here instead of silently diverging.
212
+ if (ENTRIES.length !== DATA_TOOL_NAMES.length || ENTRIES.some((entry, index) => entry.name !== DATA_TOOL_NAMES[index])) {
213
+ throw new Error('DATA_TOOL_ENTRIES_DIVERGED_FROM_DATA_SURFACE')
214
+ }
215
+
216
+ /** The five data tools, frozen, in the surface's own order. */
217
+ export const DATA_TOOL_ENTRIES = ENTRIES
218
+
219
+ /** One entry by tool name, or null when this Host does not know that name. */
220
+ export const dataToolEntry = name => ENTRIES.find(entry => entry.name === name) ?? null
221
+
222
+ const invalid = key => { throw Object.assign(new Error(`INVALID_DATA_TOOL_ARGS:${key}`), { code: 'INVALID_DATA_TOOL_ARGS' }) }
223
+
224
+ const isPlainObject = value => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
225
+ && Object.getPrototypeOf(value) === Object.prototype
226
+
227
+ const typeOf = (type, value) => type === 'string' ? typeof value === 'string'
228
+ : type === 'boolean' ? typeof value === 'boolean'
229
+ // A safe integer only: a float, a numeric string, a boolean or an unsafe
230
+ // integer is a type error, not something to coerce.
231
+ : type === 'integer' ? Number.isSafeInteger(value)
232
+ : false
233
+
234
+ const checkEnum = (schema, value, key) => { if (schema.enum && !schema.enum.includes(value)) invalid(key) }
235
+
236
+ /** The nested chart spec is validated as a nested object, not as opaque JSON. */
237
+ const validateChart = chart => {
238
+ if (!isPlainObject(chart)) invalid('chart')
239
+ const { properties, required } = CHART_INPUT_SCHEMA
240
+ if (Object.keys(chart).some(key => !Object.hasOwn(properties, key))) invalid('chart')
241
+ for (const key of required) if (!Object.hasOwn(chart, key)) invalid(`chart.${key}`)
242
+ for (const [key, value] of Object.entries(chart)) {
243
+ const schema = properties[key]
244
+ if (!typeOf(schema.type, value)) invalid(`chart.${key}`)
245
+ checkEnum(schema, value, `chart.${key}`)
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Strictly validate one data tool call's arguments against the shared contract
251
+ * and return the SAME parsed object. Nothing is coerced, nothing is defaulted
252
+ * here and no business value is invented: defaults belong to the service, which
253
+ * is the only place that knows them.
254
+ *
255
+ * Refused: a non-plain object (array, null, prototype), any key the entry does
256
+ * not declare (an authority key such as botId, or any other business key), a
257
+ * missing required key, a wrong type, a value outside an enum, an unsafe
258
+ * integer, and a JSON payload larger than MAX_ARGS_BYTES UTF-8 bytes.
259
+ */
260
+ export function parseDataToolArgs(name, args) {
261
+ const entry = dataToolEntry(name)
262
+ if (!entry) throw new TypeError(`unknown data tool: ${String(name)}`)
263
+ if (!isPlainObject(args)) invalid('args')
264
+ let bytes
265
+ try { bytes = Buffer.byteLength(JSON.stringify(args), 'utf8') } catch { invalid('args') }
266
+ if (bytes > MAX_ARGS_BYTES) invalid('args')
267
+ const { parameters } = entry
268
+ // Root additionalProperties:false — there is no authority key to pass.
269
+ if (Object.keys(args).some(key => !Object.hasOwn(parameters, key))) invalid('args')
270
+ for (const [key, spec] of Object.entries(parameters)) if (spec.required === true && !Object.hasOwn(args, key)) invalid(key)
271
+ for (const [key, value] of Object.entries(args)) {
272
+ if (key === 'chart') { validateChart(value); continue }
273
+ const schema = parameters[key]
274
+ if (!typeOf(schema.type, value)) invalid(key)
275
+ checkEnum(schema, value, key)
276
+ }
277
+ return args
278
+ }
279
+
280
+ /**
281
+ * Project the business keys of one validated call onto the existing Bot data
282
+ * service and call it exactly once. No retry, no fallback service, no second
283
+ * attempt after a failure, a timeout or a cancellation: an unknown outcome is
284
+ * reconciled by the Host, never replayed from here.
285
+ */
286
+ export function invokeDataTool(bot, name, args) {
287
+ const entry = dataToolEntry(name)
288
+ if (!entry) throw new TypeError(`unknown data tool: ${String(name)}`)
289
+ if (!bot || typeof bot[entry.method] !== 'function') throw new TypeError('DATA_TOOL_REQUIRE_ENGINE')
290
+ return bot[entry.method](entry.project(args ?? {}))
291
+ }
@@ -0,0 +1,120 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
2
+ import { desktopTicketMetadata, DESKTOP_PROXY_UPSTREAM_OPTIONS } from './desktop-proxy.mjs'
3
+
4
+ const fail=code=>{throw Object.assign(new Error(code),{code})}
5
+ const hash=value=>createHash('sha256').update(value).digest()
6
+ const token=value=>typeof value==='string'&&/^[A-Za-z0-9_-]{43}$/.test(value)
7
+ const id=value=>typeof value==='string'&&/^[-A-Za-z0-9_.]{1,128}$/.test(value)
8
+ const keys=(value,names)=>{if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).some(key=>!names.includes(key)))fail('INVALID_DESKTOP_REQUEST')}
9
+ const owner=peer=>peer?.principal?.kind==='owner'&&peer.signal&&!peer.signal.aborted
10
+
11
+ /** Native shells without a WebSocket carrier reuse owner-authenticated Fetch. A receipt acknowledges only delivered records. */
12
+ export function createDesktopHttpCarrier({now=Date.now}={}){
13
+ const rows=new Map();let closing=false
14
+ const wake=row=>{row.waiter?.();row.waiter=null}
15
+ const stop=(row,code='DESKTOP_DISCONNECTED')=>{
16
+ if(row.closed)return
17
+ row.closed=true;row.code=code;row.secret=null;clearTimeout(row.connectTimer);row.socket?.terminate();row.controller.abort();row.ownerPeer.signal.removeEventListener('abort',row.abort);wake(row)
18
+ }
19
+ const validate=row=>{
20
+ if(closing||row.closed||row.expiresAt<=now()||!owner(row.ownerPeer))fail('DESKTOP_SESSION_EXPIRED')
21
+ const result=row.validate()
22
+ if(result?.then){Promise.resolve(result).catch(()=>{});fail('DESKTOP_AUTHORITY_INVALID')}
23
+ if(result===false)fail('MACHINE_BOT_DENIED')
24
+ }
25
+ const requireRow=(input,context)=>{
26
+ if(context?.kind!=='owner'||!owner(context.peer))fail('REMOTE_OWNER_REQUIRED')
27
+ const row=rows.get(input.sessionId)
28
+ if(!row||row.ownerPeer!==context.peer||context.botId!==row.botId||context.clientOrigin!==row.origin||!token(input.token)||!timingSafeEqual(hash(input.token),row.digest))fail('DESKTOP_SESSION_DENIED')
29
+ try{validate(row)}catch(error){stop(row,error.code);throw error}
30
+ return row
31
+ }
32
+ const enqueue=(row,kind,data,binary=false)=>{
33
+ if(row.closed)return
34
+ try{validate(row)}catch(error){stop(row,error.code);return}
35
+ const bytes=Buffer.from(data)
36
+ if(bytes.length>9*1024*1024||kind==='message'&&!binary&&bytes.length>65536||row.bytes+bytes.length>12*1024*1024||row.queue.length>=64){stop(row,'DESKTOP_BACKPRESSURE');return}
37
+ row.queue.push({sequence:++row.sequence,kind,data:bytes,binary});row.bytes+=bytes.length;wake(row)
38
+ }
39
+ const start=row=>{
40
+ if(row.started)return
41
+ row.started=true;const secret=row.secret;row.secret=null
42
+ row.connectTimer=setTimeout(()=>stop(row,'DESKTOP_CONNECT_TIMEOUT'),5000);row.connectTimer.unref()
43
+ Promise.resolve().then(()=>row.connect({signal:row.controller.signal,webSocketOptions:DESKTOP_PROXY_UPSTREAM_OPTIONS})).then(socket=>{
44
+ socket.on('error',()=>stop(row,'DESKTOP_DISCONNECTED'))
45
+ if(row.closed){socket.terminate();return}
46
+ row.socket=socket
47
+ if(![0,1].includes(socket.readyState)){stop(row);return}
48
+ socket.on('message',(data,binary)=>enqueue(row,'message',data,binary))
49
+ socket.on('ping',data=>enqueue(row,'ping',data))
50
+ socket.once('close',()=>stop(row))
51
+ const auth=()=>{try{validate(row);clearTimeout(row.connectTimer);socket.send(JSON.stringify({type:'auth',sessionId:row.sessionId,token:secret}))}catch{stop(row)}}
52
+ if(socket.readyState===1)auth();else socket.once('open',auth)
53
+ }).catch(()=>stop(row,'DESKTOP_CONNECTION_FAILED'))
54
+ }
55
+ const sweep=setInterval(()=>{
56
+ for(const [key,row]of rows){if(row.closed){if(row.expiresAt<=now())rows.delete(key);continue}try{validate(row);if(row.started&&now()-row.lastPollAt>5000)stop(row,'DESKTOP_VIEWER_DISCONNECTED')}catch(error){stop(row,error.code)}}
57
+ },250);sweep.unref()
58
+ return Object.freeze({
59
+ wrapTicket(ticket,{ownerPeer,clientOrigin,botId,connect,validate:check}={}){
60
+ if(closing||!owner(ownerPeer)||!id(ticket?.sessionId)||!token(ticket.token)||!Number.isSafeInteger(ticket.expiresAt)||ticket.expiresAt<=now()||ticket.expiresAt>now()+300_000
61
+ ||!id(botId)||typeof connect!=='function'||typeof check!=='function'||clientOrigin!=='dsh-app://app')fail('DESKTOP_CARRIER_UNAVAILABLE')
62
+ let row=rows.get(ticket.sessionId)
63
+ const sourceHash=hash(ticket.token)
64
+ if(row){
65
+ if(row.closed||row.ownerPeer!==ownerPeer||row.botId!==botId||row.origin!==clientOrigin||!timingSafeEqual(row.sourceHash,sourceHash))fail('DESKTOP_SESSION_DENIED')
66
+ row.expiresAt=ticket.expiresAt;row.validate=check;row.connect=connect;validate(row)
67
+ }else{
68
+ for(const [key,value]of rows)if(value.expiresAt<=now()){stop(value);rows.delete(key)}
69
+ if(rows.size>=32)fail('DESKTOP_SESSION_LIMIT')
70
+ const proof=randomBytes(32).toString('base64url')
71
+ row={sessionId:ticket.sessionId,expiresAt:ticket.expiresAt,ownerPeer,botId,origin:clientOrigin,token:proof,digest:hash(proof),sourceHash,
72
+ secret:ticket.token,connect,validate:check,controller:new AbortController(),sequence:0,acknowledged:0,delivered:0,queue:[],bytes:0,requests:new Map(),lastInput:0,lastPollAt:now(),closed:false,polling:false}
73
+ row.abort=()=>stop(row,'DESKTOP_OWNER_DISPOSED');validate(row);ownerPeer.signal.addEventListener('abort',row.abort,{once:true});rows.set(row.sessionId,row)
74
+ }
75
+ return Object.freeze({...desktopTicketMetadata(ticket),carrier:'owner-http',token:row.token})
76
+ },
77
+ async poll(input,context){
78
+ keys(input,['sessionId','token','cursor'])
79
+ const row=requireRow(input,context)
80
+ if(!Number.isSafeInteger(input.cursor)||input.cursor<row.acknowledged||input.cursor>row.delivered)fail('DESKTOP_CURSOR_INVALID')
81
+ if(row.polling)fail('DESKTOP_POLL_PENDING')
82
+ row.polling=true;row.lastPollAt=now()
83
+ try{
84
+ while(row.queue.length&&row.queue[0].sequence<=input.cursor){
85
+ const item=row.queue.shift();row.bytes-=item.data.length
86
+ if(item.kind==='ping'&&row.socket?.readyState===1)row.socket.pong(item.data)
87
+ }
88
+ row.acknowledged=input.cursor;start(row)
89
+ if(!row.queue.length&&!row.closed)await new Promise(resolve=>{
90
+ const done=()=>{clearTimeout(timer);context.signal?.removeEventListener('abort',done);if(row.waiter===done)row.waiter=null;resolve()}
91
+ const timer=setTimeout(done,1000);row.waiter=done;context.signal?.addEventListener('abort',done,{once:true});if(context.signal?.aborted)done()
92
+ })
93
+ context.signal?.throwIfAborted()
94
+ if(row.closed)return {events:[],cursor:input.cursor,closed:true,code:row.code}
95
+ validate(row)
96
+ const events=row.queue.slice(0,12).map(item=>({sequence:item.sequence,kind:item.kind,binary:item.binary,data:item.data.toString('base64')}))
97
+ const cursor=events.at(-1)?.sequence??input.cursor;row.delivered=Math.max(row.delivered,cursor)
98
+ return {events,cursor,closed:false}
99
+ }finally{row.polling=false;if(context.signal?.aborted)stop(row,'DESKTOP_VIEWER_DISCONNECTED')}
100
+ },
101
+ async send(input,context){
102
+ keys(input,['sessionId','token','requestId','message'])
103
+ const row=requireRow(input,context)
104
+ const sequence=typeof input.requestId==='string'&&/^http-desktop-[1-9]\d{0,14}$/.test(input.requestId)?Number(input.requestId.slice('http-desktop-'.length)):NaN
105
+ if(!Number.isSafeInteger(sequence)||typeof input.message!=='string'||Buffer.byteLength(input.message)>65536)fail('INVALID_DESKTOP_REQUEST')
106
+ let message;try{message=JSON.parse(input.message)}catch{fail('INVALID_DESKTOP_REQUEST')}
107
+ if(!message||!['control','input','release-input','close'].includes(message.type))fail('INVALID_DESKTOP_REQUEST')
108
+ const digest=hash(input.message).toString('hex'),previous=row.requests.get(input.requestId)
109
+ if(previous){if(previous!==digest)fail('DESKTOP_REQUEST_CONFLICT');return {accepted:true}}
110
+ if(sequence!==row.lastInput+1)fail('DESKTOP_INPUT_REPLAY')
111
+ if(row.socket?.readyState!==1)fail('DESKTOP_NOT_READY')
112
+ row.lastInput=sequence;row.requests.set(input.requestId,digest)
113
+ while(row.requests.size>256)row.requests.delete(row.requests.keys().next().value)
114
+ try{await new Promise((resolve,reject)=>row.socket.send(input.message,error=>error?reject(error):resolve()));return {accepted:true}}
115
+ catch{stop(row,'DESKTOP_INPUT_UNKNOWN');fail('DESKTOP_INPUT_UNKNOWN')}
116
+ },
117
+ revoke(sessionId){const row=rows.get(sessionId);if(row)stop(row,'DESKTOP_REVOKED')},
118
+ close(){if(closing)return;closing=true;clearInterval(sweep);for(const row of rows.values())stop(row);rows.clear()},
119
+ })
120
+ }