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