@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,186 @@
1
+ /**
2
+ * Bounded data import/export for the Bot data tools.
3
+ *
4
+ * Import parses CSV/TSV or JSON/NDJSON text — the text already came from a
5
+ * bounded controlled read (BotLibrary) or bounded inline content, never from
6
+ * an OS path or URL. It infers a light column kind so SQLite can hold numbers
7
+ * as numbers, and it enforces row/column/byte caps so one file cannot exhaust
8
+ * memory. Export writes CSV that neutralises spreadsheet formula injection.
9
+ */
10
+
11
+ import { DATA_LIMITS, DataFailure, dataError } from './bot-data-surface.mjs'
12
+
13
+ const fail = (code, message, extra = {}) => { throw new DataFailure(dataError(code, message, extra)) }
14
+
15
+ /** Column kind inferred from parsed values; SQLite affinity is chosen from it. */
16
+ const SQLITE_TYPE = { integer: 'INTEGER', real: 'REAL', boolean: 'INTEGER', text: 'TEXT', null: 'TEXT' }
17
+
18
+ const normalizeHeader = (raw, index, seen) => {
19
+ let name = String(raw ?? '').trim()
20
+ if (name === '') name = `column_${index + 1}`
21
+ let unique = name
22
+ for (let n = 2; seen.has(unique.toLowerCase()); n++) unique = `${name}_${n}`
23
+ seen.add(unique.toLowerCase())
24
+ return unique
25
+ }
26
+
27
+ /**
28
+ * Parse RFC-4180-ish delimited text. Handles quoted fields with embedded
29
+ * delimiters, newlines and doubled quotes. Returns { columns, rows }.
30
+ */
31
+ export function parseDelimited(text, { delimiter = ',', header = true } = {}) {
32
+ const rows = []
33
+ let field = ''
34
+ let row = []
35
+ let inQuotes = false
36
+ let i = 0
37
+ const n = text.length
38
+ const pushField = () => { row.push(field); field = '' }
39
+ const pushRow = () => { pushField(); if (row.length !== 1 || row[0] !== '') rows.push(row); row = [] }
40
+ while (i < n) {
41
+ const ch = text[i]
42
+ if (inQuotes) {
43
+ if (ch === '"') { if (text[i + 1] === '"') { field += '"'; i += 2; continue } inQuotes = false; i++; continue }
44
+ field += ch; i++; continue
45
+ }
46
+ if (ch === '"') { inQuotes = true; i++; continue }
47
+ if (ch === delimiter) { pushField(); i++; continue }
48
+ if (ch === '\r') { if (text[i + 1] === '\n') i++; pushRow(); i++; continue }
49
+ if (ch === '\n') { pushRow(); i++; continue }
50
+ field += ch; i++
51
+ }
52
+ pushRow()
53
+ if (inQuotes) fail('import_failed', 'Unterminated quoted field in the input.')
54
+ if (!rows.length) fail('import_failed', 'The input has no rows.')
55
+
56
+ const seen = new Set()
57
+ let columns
58
+ let dataRows
59
+ if (header) {
60
+ columns = rows[0].map((cell, index) => normalizeHeader(cell, index, seen))
61
+ dataRows = rows.slice(1)
62
+ } else {
63
+ const width = rows[0].length
64
+ columns = Array.from({ length: width }, (_, index) => normalizeHeader(`column_${index + 1}`, index, seen))
65
+ dataRows = rows
66
+ }
67
+ return finish(columns, dataRows, delimiter)
68
+ }
69
+
70
+ /** Parse JSON (array of objects, single object) or NDJSON (one object per line). */
71
+ export function parseJsonText(text) {
72
+ const trimmed = text.trim()
73
+ let records
74
+ let whole = null
75
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
76
+ try { whole = JSON.parse(text) } catch { whole = null }
77
+ }
78
+ if (whole !== null) {
79
+ records = Array.isArray(whole) ? whole : [whole]
80
+ if (Array.isArray(whole) && whole.length && whole.every(item => typeof item !== 'object' || item === null || Array.isArray(item))) {
81
+ // An array of scalars: one column named "value".
82
+ return finish(['value'], whole.map(item => [coerce(item)]))
83
+ }
84
+ } else {
85
+ // NDJSON: one JSON value per line.
86
+ records = []
87
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
88
+ const t = line.trim()
89
+ if (t === '') continue
90
+ try { records.push(JSON.parse(t)) } catch (error) { fail('import_failed', `NDJSON line ${index + 1} parse failed: ${error.message}`) }
91
+ }
92
+ }
93
+ if (!records.length) fail('import_failed', 'The input has no records.')
94
+ const objects = records.filter(record => record !== null && typeof record === 'object' && !Array.isArray(record))
95
+ if (!objects.length) fail('import_failed', 'The input has no JSON objects.')
96
+ const seen = new Set()
97
+ // column order = first-seen key order; original key kept for lookup, lower for dedupe.
98
+ const columnKeys = []
99
+ const columnLower = new Set()
100
+ for (const object of objects) {
101
+ for (const key of Object.keys(object)) {
102
+ const lower = key.toLowerCase()
103
+ if (columnLower.has(lower)) continue
104
+ columnLower.add(lower)
105
+ columnKeys.push({ key, name: normalizeHeader(key, columnKeys.length, seen) })
106
+ }
107
+ }
108
+ const columns = columnKeys.map(entry => entry.name)
109
+ const dataRows = objects.map(object => columnKeys.map(entry => (object[entry.key] === undefined ? null : normalizeCell(object[entry.key]))))
110
+ return finish(columns, dataRows)
111
+ }
112
+
113
+ const coerce = value => (value === null || value === undefined ? null : typeof value === 'object' ? JSON.stringify(value) : value)
114
+
115
+ const normalizeCell = value => (value === undefined ? null : coerce(value))
116
+
117
+ /** Infer each column's final kind across all rows, then convert every cell to it. */
118
+ function finish(columns, dataRows, delimiter) {
119
+ if (columns.length > DATA_LIMITS.loadMaxCols) fail('source_unsupported', `More than ${DATA_LIMITS.loadMaxCols} columns.`, { hint: 'Select fewer columns before loading.' })
120
+ if (dataRows.length > DATA_LIMITS.loadMaxRows) fail('source_unsupported', `More than ${DATA_LIMITS.loadMaxRows} rows.`, { hint: 'Filter or sample the data before loading.' })
121
+
122
+ // Pass 1: classify each raw value and widen the column kind.
123
+ const kinds = new Array(columns.length).fill('null')
124
+ const cellKinds = dataRows.map(row => row.map(raw => {
125
+ let kind
126
+ if (raw === '' || raw === null || raw === undefined) kind = 'null'
127
+ else if (typeof raw === 'boolean') kind = 'boolean'
128
+ else if (typeof raw === 'number') kind = Number.isInteger(raw) ? 'integer' : 'real'
129
+ else if (typeof raw === 'string' && /^\s*-?\d+\s*$/.test(raw)) kind = 'integer'
130
+ else if (typeof raw === 'string' && /^\s*-?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?\s*$/.test(raw)) kind = 'real'
131
+ else kind = 'text'
132
+ return kind
133
+ }))
134
+ for (const row of cellKinds) row.forEach((kind, c) => { kinds[c] = widen(kinds[c], kind) })
135
+
136
+ // Pass 2: convert each raw cell to its column's final kind.
137
+ const rows = dataRows.map((row, r) => row.map((raw, c) => {
138
+ const kind = kinds[c]
139
+ if (raw === '' || raw === null || raw === undefined) return null
140
+ if (kind === 'integer') { if (typeof raw === 'boolean') return raw ? 1 : 0; return typeof raw === 'number' ? raw : Number(String(raw).trim()) }
141
+ if (kind === 'real') return typeof raw === 'number' ? raw : Number(String(raw).trim())
142
+ if (kind === 'boolean') return raw ? 1 : 0
143
+ return typeof raw === 'object' ? JSON.stringify(raw) : String(raw)
144
+ }))
145
+ return { columns, rows, kinds, sqlTypes: kinds.map(kind => SQLITE_TYPE[kind]) }
146
+ }
147
+
148
+ /** Widen a column kind as more values arrive: null < integer < real < text. */
149
+ const RANK = { null: 0, boolean: 1, integer: 1, real: 2, text: 3 }
150
+ const widen = (a, b) => {
151
+ if (a === b) return a
152
+ if (a === 'null') return b === 'boolean' ? 'integer' : b
153
+ if (b === 'null') return a
154
+ if ((a === 'integer' && b === 'boolean') || (a === 'boolean' && b === 'integer')) return 'integer'
155
+ if (RANK[a] >= RANK[b]) return a === 'integer' && b === 'real' ? 'real' : a
156
+ return b === 'integer' && a === 'real' ? 'real' : b
157
+ }
158
+
159
+ /**
160
+ * Encode one CSV field: neutralise spreadsheet formula injection, then quote
161
+ * when the value contains a delimiter, quote, newline or edge whitespace.
162
+ * A leading `-` is only neutralised when the value is not a plain number.
163
+ */
164
+ export function csvField(value) {
165
+ let s = value === null || value === undefined ? '' : String(value)
166
+ const dangerous = /^[=+@\t\r]/.test(s) || (/^-/.test(s) && !/^\s*-?\d+(\.\d+)?([eE][+-]?\d+)?\s*$/.test(s))
167
+ if (dangerous) s = `'${s}`
168
+ if (/[",\n\r]/.test(s) || s !== s.trim()) s = `"${s.replace(/"/g, '""')}"`
169
+ return s
170
+ }
171
+
172
+ /** Encode rows (array of cell arrays) to CSV text with a header row. */
173
+ export function toCsv(columns, rows) {
174
+ const lines = [columns.map(column => csvField(column.name ?? column)).join(',')]
175
+ for (const row of rows) lines.push(row.map(cell => csvField(cell)).join(','))
176
+ return `${lines.join('\r\n')}\r\n`
177
+ }
178
+
179
+ /** Strip NUL and cap bytes of an exported payload; never fabricate content. */
180
+ export function boundExport(text, maxBytes = DATA_LIMITS.exportMaxBytes) {
181
+ const bytes = Buffer.byteLength(text)
182
+ if (bytes <= maxBytes) return { text, bytes, truncated: false }
183
+ // Cut on a UTF-8-safe boundary.
184
+ const buffer = Buffer.from(text, 'utf8').subarray(0, maxBytes)
185
+ return { text: buffer.toString('utf8'), bytes: buffer.length, truncated: true }
186
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The SQL gate: what a Bot may run against its own database.
3
+ *
4
+ * This is a tokenizer-based statement classifier, NOT a keyword regex. It walks
5
+ * the statement once, masking string literals, comments and quoted identifiers
6
+ * so a keyword inside a string can never change the verdict, and it reads the
7
+ * verb after a WITH clause so a `WITH … DELETE` never passes as a read. The
8
+ * defence-in-depth (extension loading disabled, no readfile/writefile, ATTACH
9
+ * refused here) is proven by execution in tests/security-negative, not asserted.
10
+ *
11
+ * Only SELECT-like reads, and — when the caller allows it — `CREATE … AS`
12
+ * (TABLE|VIEW), run. Everything else (INSERT/UPDATE/DELETE/DROP/ALTER/ATTACH/
13
+ * DETACH/PRAGMA/VACUUM/BEGIN/…) is refused. One statement per call.
14
+ *
15
+ * Ported and narrowed from OpenMausBot server/data/tools.ts (commit 1bc4749)
16
+ * for SQLite: no dollar-quoting, no non-nesting block comments, no E-strings.
17
+ */
18
+
19
+ import { DataFailure, dataError } from './bot-data-surface.mjs'
20
+
21
+ const fail = (code, message, extra = {}) => { throw new DataFailure(dataError(code, message, extra)) }
22
+
23
+ const READ_VERBS = new Set(['SELECT', 'WITH', 'VALUES', 'EXPLAIN'])
24
+ const MATERIALISABLE = new Set(['SELECT', 'WITH', 'VALUES'])
25
+
26
+ /** A token: word (upper-cased text), ident (quoted), block (parenthesised), punct. */
27
+ const tokenize = sql => {
28
+ const tokens = []
29
+ let statements = 1
30
+ let ended = false
31
+ let depth = 0
32
+ let i = 0
33
+ const n = sql.length
34
+ const push = (kind, raw, text = raw) => { if (depth === 0) tokens.push({ kind, raw, text }) }
35
+ while (i < n) {
36
+ const ch = sql[i]
37
+ const next = sql[i + 1]
38
+ if (ch === '-' && next === '-') { const end = sql.slice(i).search(/[\n\r]/); i = end === -1 ? n : i + end + 1; continue }
39
+ if (ch === '/' && next === '*') { i = blockCommentEnd(sql, i); continue }
40
+ if (ch === ';') { ended = true; i++; continue }
41
+ if (ended && /\S/.test(ch)) { statements++; ended = false }
42
+ if (ch === "'") { push('word', "'…'"); i = literalEnd(sql, i); continue }
43
+ if (ch === '"' || ch === '`') { const j = quotedEnd(sql, i, ch); push('ident', sql.slice(i, j)); i = j; continue }
44
+ if (ch === '[') { const j = bracketEnd(sql, i); push('ident', sql.slice(i, j)); i = j; continue }
45
+ if (ch === '(') { push('block', '(…)'); depth++; i++; continue }
46
+ if (ch === ')') { depth = Math.max(0, depth - 1); i++; continue }
47
+ if (/[A-Za-z_]/.test(ch)) { let j = i + 1; while (j < n && /[A-Za-z0-9_$]/.test(sql[j])) j++; push('word', sql.slice(i, j), sql.slice(i, j).toUpperCase()); i = j; continue }
48
+ if (ch === ',') { push('punct', ','); i++; continue }
49
+ i++
50
+ }
51
+ return { tokens, statements }
52
+ }
53
+
54
+ /** SQLite block comments do not nest. */
55
+ const blockCommentEnd = (sql, start) => {
56
+ const end = sql.indexOf('*/', start + 2)
57
+ if (end === -1) fail('invalid_input', 'Unterminated comment.')
58
+ return end + 2
59
+ }
60
+
61
+ /** A single-quoted literal: '' escapes a quote. */
62
+ const literalEnd = (sql, quote) => {
63
+ for (let j = quote + 1; j < sql.length; j++) {
64
+ if (sql[j] !== "'") continue
65
+ if (sql[j + 1] === "'") { j++; continue }
66
+ return j + 1
67
+ }
68
+ fail('invalid_input', 'Unterminated string literal.')
69
+ }
70
+
71
+ /** A double-quoted or backtick identifier; the quote character doubles to escape. */
72
+ const quotedEnd = (sql, start, quote) => {
73
+ for (let j = start + 1; j < sql.length; j++) {
74
+ if (sql[j] !== quote) continue
75
+ if (sql[j + 1] === quote) { j++; continue }
76
+ return j + 1
77
+ }
78
+ fail('invalid_input', 'Unterminated quoted identifier.')
79
+ }
80
+
81
+ /** A [bracketed] identifier (SQLite/MSSQL style); ]] escapes a bracket. */
82
+ const bracketEnd = (sql, start) => {
83
+ for (let j = start + 1; j < sql.length; j++) {
84
+ if (sql[j] === ']') { if (sql[j + 1] === ']') { j++; continue } return j + 1 }
85
+ }
86
+ fail('invalid_input', 'Unterminated bracketed identifier.')
87
+ }
88
+
89
+ export const classifySql = sql => {
90
+ if (typeof sql !== 'string' || sql.trim() === '') fail('invalid_input', 'The statement is empty.')
91
+ const { tokens, statements } = tokenize(sql)
92
+ if (statements > 1) fail('invalid_input', 'One statement per call.', { hint: 'Send each statement in its own data_sql call.' })
93
+ if (!tokens.length || tokens[0].kind !== 'word') fail('invalid_input', 'The statement is empty.')
94
+ return classifyTokens(tokens)
95
+ }
96
+
97
+ const classifyTokens = tokens => {
98
+ let i = 0
99
+ let verb = tokens[0].text
100
+ const withClause = verb === 'WITH'
101
+ if (withClause) {
102
+ // WITH [RECURSIVE] name [(cols)] AS (body) [, …] <verb>
103
+ i = 1
104
+ if (tokens[i]?.text === 'RECURSIVE') i++
105
+ for (;;) {
106
+ while (i < tokens.length && tokens[i].kind !== 'block') i++
107
+ i++
108
+ if (tokens[i]?.text === 'AS') { while (i < tokens.length && tokens[i].kind !== 'block') i++; i++ }
109
+ if (tokens[i]?.kind === 'punct') { i++; continue }
110
+ break
111
+ }
112
+ verb = tokens[i]?.kind === 'word' ? tokens[i].text : ''
113
+ if (!verb) fail('invalid_input', 'A WITH clause needs a statement after it.')
114
+ if (verb === 'WITH') return { kind: 'refused', verb }
115
+ }
116
+ if (verb === 'EXPLAIN') {
117
+ // EXPLAIN shows the plan; EXPLAIN QUERY PLAN too. Neither runs the statement.
118
+ return { kind: 'read', materialisable: false }
119
+ }
120
+ if (READ_VERBS.has(verb)) return { kind: 'read', materialisable: withClause || MATERIALISABLE.has(verb) }
121
+ if (verb === 'CREATE') {
122
+ let j = i + 1
123
+ if (tokens[j]?.text === 'TEMP' || tokens[j]?.text === 'TEMPORARY') j++
124
+ // CREATE [TEMP] [OR REPLACE] TABLE|VIEW [IF NOT EXISTS] name AS select
125
+ const orReplace = tokens[j]?.text === 'OR' && tokens[j + 1]?.text === 'REPLACE'
126
+ if (orReplace) j += 2
127
+ const object = tokens[j]?.text
128
+ if (object !== 'TABLE' && object !== 'VIEW') return { kind: 'refused', verb: `CREATE ${object ?? ''}`.trim() }
129
+ j++
130
+ if (tokens[j]?.text === 'IF' && tokens[j + 1]?.text === 'NOT' && tokens[j + 2]?.text === 'EXISTS') j += 3
131
+ const nameParts = []
132
+ while (j < tokens.length && tokens[j].kind !== 'block' && tokens[j].text !== 'AS') { nameParts.push(tokens[j].raw.replace(/\.$/, '')); j++ }
133
+ if (tokens[j]?.kind === 'block') j++
134
+ if (tokens[j]?.text !== 'AS' || !nameParts.length) return { kind: 'refused', verb: `CREATE ${object}` }
135
+ return { kind: 'create', target: nameParts.join('.'), object: object.toLowerCase(), orReplace }
136
+ }
137
+ return { kind: 'refused', verb }
138
+ }
139
+
140
+ /** Throws write_refused for anything but a read (and, when allowed, a CREATE … AS). */
141
+ export const gateSql = (sql, { allowCreate = false } = {}) => {
142
+ const kind = classifySql(sql)
143
+ if (kind.kind === 'refused') {
144
+ throw new DataFailure(dataError('write_refused', `${kind.verb} is not allowed here.`, {
145
+ sql,
146
+ hint: allowCreate
147
+ ? 'Only SELECT-like statements and CREATE [OR REPLACE] TABLE|VIEW … AS … run here. Load data with data_load; export with data_export.'
148
+ : 'Only SELECT-like statements run here.',
149
+ }))
150
+ }
151
+ if (kind.kind === 'create' && !allowCreate) {
152
+ throw new DataFailure(dataError('write_refused', 'CREATE is not allowed here.', { sql, hint: 'Run CREATE TABLE … AS with data_sql, then show or export the table.' }))
153
+ }
154
+ return kind
155
+ }
156
+
157
+ const CELL_CHARS_MAX = 4000
158
+
159
+ /** Trim a cell's text and bound the result by rows and bytes. */
160
+ export function boundRows(rows, maxRows, maxBytes) {
161
+ let truncated = rows.length > maxRows
162
+ let kept = rows.slice(0, maxRows).map(row => row.map(cell => (typeof cell === 'string' && cell.length > CELL_CHARS_MAX ? `${cell.slice(0, CELL_CHARS_MAX)}…` : cell)))
163
+ while (kept.length && Buffer.byteLength(JSON.stringify(kept)) > maxBytes) {
164
+ kept = kept.slice(0, Math.max(0, kept.length - Math.max(1, Math.ceil(kept.length / 10))))
165
+ truncated = true
166
+ }
167
+ return { rows: kept, truncated }
168
+ }
169
+
170
+ const trimSample = sample => sample?.slice(0, 3).map(value => (value.length > 60 ? `${value.slice(0, 60)}…` : value))
171
+
172
+ /** Bound a column list by count and bytes, degrading to name+type under pressure. */
173
+ export function boundColumns(columns, maxBytes, columnsMax = 100) {
174
+ let truncated = columns.length > columnsMax
175
+ let kept = truncated
176
+ ? columns.map(({ name, type }) => ({ name, type }))
177
+ : columns.map(column => ({ ...column, ...(column.sample ? { sample: trimSample(column.sample) } : {}) }))
178
+ const size = () => Buffer.byteLength(JSON.stringify(kept))
179
+ if (size() > maxBytes) { kept = kept.map(({ sample: _sample, ...rest }) => rest); truncated = true }
180
+ if (size() > maxBytes) kept = kept.map(({ min: _min, max: _max, ...rest }) => rest)
181
+ if (size() > maxBytes) kept = kept.map(({ name, type }) => ({ name, type }))
182
+ while (kept.length && size() > maxBytes) kept = kept.slice(0, Math.max(0, kept.length - Math.max(1, Math.ceil(kept.length / 10))))
183
+ return { columns: kept, truncated }
184
+ }
185
+
186
+ /** Map an engine error to a structured DataError; a killed child reads as cancelled/timeout. */
187
+ export function toDataError(error, sql, { aborted = false } = {}) {
188
+ if (error instanceof DataFailure) return { ...error.error, ...(sql && !error.error.sql ? { sql } : {}) }
189
+ const message = error instanceof Error ? error.message : String(error)
190
+ if (aborted) return { code: 'cancelled', message: 'The statement was cancelled.', ...(sql ? { sql } : {}) }
191
+ if (/timed out|timeout|SIGKILL|killed/i.test(message)) return { code: 'timeout', message, retryable: true, ...(sql ? { sql } : {}) }
192
+ if (/interrupt/i.test(message)) return { code: 'cancelled', message: 'The statement was cancelled.', ...(sql ? { sql } : {}) }
193
+ const line = /\bLINE (\d+):/.exec(message)
194
+ return { code: 'sql_error', message, ...(sql ? { sql } : {}), ...(line ? { line: Number(line[1]) } : {}) }
195
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The Data surface for the model-router Bot runtime: what the tools, the
3
+ * engine, the fixed worker and the panel agree on. One SQLite database per
4
+ * Bot, addressed only by a validated Bot id the Host derives from committed
5
+ * state — never from a model argument.
6
+ *
7
+ * This mirrors OpenMausBot's `shared/data-surface.ts` shape (commit 1bc4749)
8
+ * so the parent session can later swap the SQLite engine for DuckDB without
9
+ * changing the tool contract. It is intentionally smaller: no DuckDB-only
10
+ * readers, no external files/URLs — data enters only through a bounded
11
+ * controlled read or bounded inline content.
12
+ */
13
+
14
+ /** The five tools a Bot gets. */
15
+ export const DATA_TOOL_NAMES = Object.freeze(['data_load', 'data_describe', 'data_sql', 'data_show', 'data_export'])
16
+
17
+ /** Bounds. Every result is capped by rows AND bytes; the cap is the contract, not a plan. */
18
+ export const DATA_LIMITS = Object.freeze({
19
+ /** data_sql rows returned to the model by default, and at most. */
20
+ sqlRowsDefault: 100,
21
+ sqlRowsMax: 1000,
22
+ /** Bytes of rows data_sql returns to the model. */
23
+ sqlBytesMax: 256 * 1024,
24
+ /** data_describe result bytes; beyond describeColumnsMax columns, types only. */
25
+ describeBytesMax: 32 * 1024,
26
+ describeColumnsMax: 100,
27
+ /** Sample values per column, each at most sampleChars characters. */
28
+ sampleValues: 3,
29
+ sampleChars: 60,
30
+ /** Rows per page the panel's grid asks for. */
31
+ pageSize: 200,
32
+ pageLimitMax: 2000,
33
+ /** Marks a chart may render after its SQL reduction. */
34
+ chartMaxMarks: 5000,
35
+ /** Rows data_show kind:"table" keeps for the panel. */
36
+ showTableRowsMax: 100_000,
37
+ /** One statement's wall clock before the child is killed. */
38
+ sqlTimeoutMs: 30_000,
39
+ /** Cards a sheet holds; the oldest unpinned card goes first. */
40
+ sheetCardsMax: 100,
41
+ /** Inline content / library read byte cap for a single data_load. */
42
+ loadMaxBytes: 8 * 1024 * 1024,
43
+ /** Rows a single data_load may materialise. */
44
+ loadMaxRows: 200_000,
45
+ /** Columns a single data_load may materialise. */
46
+ loadMaxCols: 512,
47
+ /** A single cell's text length before it is truncated for display. */
48
+ cellCharsMax: 4000,
49
+ /** Exported CSV/JSON byte cap. */
50
+ exportMaxBytes: 16 * 1024 * 1024,
51
+ /**
52
+ * Native/database bounds enforced INSIDE the fixed worker. Row/character caps
53
+ * alone do not bound SQLite: a single statement can allocate native memory or
54
+ * grow the database file without returning many rows, so the worker sets these
55
+ * at open time and SQLite itself refuses (SQLITE_FULL / SQLITE_NOMEM) instead
56
+ * of the Host growing without limit.
57
+ */
58
+ /** Pages one Bot's database file may hold (SQLITE_FULL beyond it). */
59
+ dbMaxPageCount: 32_768,
60
+ /** Native heap SQLite refuses to exceed (SQLITE_NOMEM beyond it). */
61
+ dbHardHeapLimitBytes: 96 * 1024 * 1024,
62
+ /** Native heap SQLite starts reclaiming at. */
63
+ dbSoftHeapLimitBytes: 48 * 1024 * 1024,
64
+ /** The worker child's V8 old space; a runaway JS allocation dies, not the Host. */
65
+ workerHeapMb: 256,
66
+ /** Bot databases resident at once; the least recently used worker is closed. */
67
+ activeWorkersMax: 4,
68
+ /** How long close() waits for a child's real exit before reporting it. */
69
+ workerExitTimeoutMs: 10_000,
70
+ /** Wall clock for opening a database in a fresh child. */
71
+ openTimeoutMs: 15_000,
72
+ })
73
+
74
+ /** Where materialised card results live inside a Bot's database. */
75
+ export const DATA_RESULTS_SCHEMA = 'mb_data_results'
76
+
77
+ export const DATA_ERROR_CODES = Object.freeze([
78
+ 'invalid_input', 'source_not_found', 'source_unsupported', 'sql_error', 'write_refused',
79
+ 'table_not_found', 'spec_invalid', 'card_not_found', 'output_too_large', 'timeout', 'cancelled',
80
+ 'engine_unavailable', 'import_failed', 'dataset_conflict', 'unauthorized',
81
+ ])
82
+
83
+ /**
84
+ * Bound an engine/worker message so a failure never leaks the Host's layout or
85
+ * secrets: absolute paths are collapsed, stack frames are dropped and the text
86
+ * is capped. `secrets` are literal strings (a data directory, a temp directory)
87
+ * that must never reach the model or the panel.
88
+ */
89
+ export function sanitizeDataMessage(message, secrets = [], maxChars = 400) {
90
+ let text = String(message ?? '')
91
+ text = text.replace(/(?:\r?\n)+\s*at\s+[^\r\n]*/g, '') // stack frames
92
+ text = text.replace(/[A-Za-z]:[\\/][^\s'"`,;)]*/g, '<path>') // Windows absolute paths
93
+ text = text.replace(/(^|[\s'"=(,])\/(?:[^\s'"`,;)]{2,})/g, '$1<path>') // POSIX absolute paths
94
+ for (const secret of secrets) {
95
+ if (typeof secret !== 'string' || secret.length < 3) continue
96
+ text = text.split(secret).join('<redacted>')
97
+ }
98
+ text = text.replace(/[\r\n]+/g, ' ').replace(/\s{2,}/g, ' ').trim()
99
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text
100
+ }
101
+
102
+ /** A structured failure every tool and route returns. `message` is the engine's own text. */
103
+ export function dataError(code, message, extra = {}) {
104
+ if (!DATA_ERROR_CODES.includes(code)) throw new RangeError(`unknown data error code: ${code}`)
105
+ return { code, message, ...extra }
106
+ }
107
+
108
+ /** The throwable failure used inside the pure modules. */
109
+ export class DataFailure extends Error {
110
+ constructor(error) {
111
+ super(error.message)
112
+ this.name = 'DataFailure'
113
+ this.error = error
114
+ }
115
+ }
116
+
117
+ /** A table name accepted as a load target / result name. */
118
+ export const IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/
119
+ /** A Bot id, matching the Host's principal directory rules. */
120
+ export const BOT_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/
121
+
122
+ /** Quote a SQL identifier for SQLite. */
123
+ export const quoteIdent = name => `"${String(name).replace(/"/g, '""')}"`
124
+ /** Quote a SQL string literal for SQLite. */
125
+ export const quoteLiteral = value => `'${String(value).replace(/'/g, "''")}'`
126
+
127
+ /**
128
+ * Derive a safe table name from an arbitrary stem (file name, dataset label).
129
+ * Always returns a plain identifier safe to embed as a quoted SQL identifier.
130
+ */
131
+ export function tableNameFrom(stem) {
132
+ let name = String(stem ?? '').toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 63)
133
+ if (!name) name = 'data'
134
+ if (!/^[a-z_]/.test(name)) name = `t_${name}`.slice(0, 63)
135
+ return name
136
+ }
137
+
138
+ /** Validate a Bot id, throwing a DataFailure otherwise. */
139
+ export function assertBotId(botId) {
140
+ if (typeof botId !== 'string' || !BOT_ID.test(botId) || /[. ]$/.test(botId)) {
141
+ throw new DataFailure(dataError('invalid_input', 'A valid Bot id is required; it comes from the Host, never from an argument.'))
142
+ }
143
+ return botId
144
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The five data tools a Bot can call: data_load, data_describe, data_sql,
3
+ * data_show, data_export. They are the model-facing half of the loop; the
4
+ * panel's owner actions (bot-data-actions.mjs) are the other half.
5
+ *
6
+ * Authority: the calling Bot's id comes from the live session through the
7
+ * injected `botIdFor(exec)` — never from an argument. A Bot only ever reaches
8
+ * its own database; it cannot name another Bot, an OS path, a program, a
9
+ * database file or a URL. Data enters only from a bounded controlled library
10
+ * read or bounded inline content. Every failure is a structured DataError the
11
+ * model can read, never a silent success.
12
+ *
13
+ * `botIdFor` is the Host's grant check (see data-product-access.mjs): a Bot
14
+ * without the `tool/data` grant, or one whose configuration is not committed,
15
+ * gets `unauthorized` with the Host's own reason instead of a vague refusal.
16
+ *
17
+ * Names, descriptions, the SDK parameter map and the business projection all
18
+ * come from the shared contract (data-tool-contract.mjs), which the native MCP
19
+ * door (native-data-tools.mjs) reads too: the two doors cannot drift apart, and
20
+ * a model-supplied `botId` is projected away rather than accepted as authority.
21
+ */
22
+
23
+ import { defineTool } from '@deepseek-ai/dsh-tools'
24
+ import { DataFailure, sanitizeDataMessage } from './bot-data-surface.mjs'
25
+ import { DATA_TOOL_ENTRIES, invokeDataTool } from './data-tool-contract.mjs'
26
+
27
+ const JSON_OUTPUT = {
28
+ schema: { type: 'json' },
29
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
30
+ }
31
+
32
+ const errorOf = error => (error instanceof DataFailure ? error.error : { code: 'engine_unavailable', message: String(error?.message ?? error) })
33
+
34
+ /**
35
+ * @param {object} ctx Host tool registry ({ tools: { register } }).
36
+ * @param {{ engine: object, botIdFor: (exec: object) => string | null }} source
37
+ * @returns {() => void} release
38
+ */
39
+ export function registerDataTools(ctx, { engine, botIdFor } = {}) {
40
+ if (!engine || typeof engine.forBot !== 'function') throw new TypeError('DATA_TOOLS_REQUIRE_ENGINE')
41
+ if (typeof botIdFor !== 'function') throw new TypeError('DATA_TOOLS_REQUIRE_BOT_ID_FOR')
42
+ if (!ctx || typeof ctx.tools?.register !== 'function') throw new TypeError('DATA_TOOLS_REQUIRE_CTX')
43
+
44
+ const asBot = async (exec, run) => {
45
+ let botId = null
46
+ // A Bot without the data grant, or one whose configuration is not committed
47
+ // yet, is refused HERE with the Host's own reason — not as a vague
48
+ // "unknown subject".
49
+ try { botId = botIdFor(exec) ?? null } catch (error) {
50
+ return { error: { code: 'unauthorized', message: sanitizeDataMessage(error?.message ?? error) } }
51
+ }
52
+ if (typeof botId !== 'string' || botId === '') return { error: { code: 'unknown_subject', message: 'The calling Bot could not be identified from the live session.' } }
53
+ try { return await run(engine.forBot(botId), botId) } catch (error) { return { error: errorOf(error) } }
54
+ }
55
+
56
+ const tools = DATA_TOOL_ENTRIES.map(entry => defineTool({
57
+ name: entry.name,
58
+ description: entry.description,
59
+ parameters: entry.parameters,
60
+ output: JSON_OUTPUT,
61
+ // Business arguments only: the projection drops anything the model added,
62
+ // including a botId, so it can never be read as authority.
63
+ execute: (args, exec) => asBot(exec, (bot, botId) => invokeDataTool(bot, entry.name, args)),
64
+ }))
65
+
66
+ const releases = tools.map(tool => ctx.tools.register(tool))
67
+ return () => { for (const release of releases.reverse()) release() }
68
+ }