@ljwei-stak/dsh-model-route-bot 0.66.4 → 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.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +18 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- 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
|
+
}
|