@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +20 -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/README.md +3 -3
- 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
|
@@ -1,1001 +1,1559 @@
|
|
|
1
|
-
import { createBotRoutingService } from './bot-routing-service.mjs'
|
|
2
|
-
import { openBotRoutingSettings } from './bot-routing-settings.mjs'
|
|
3
|
-
import { createWorkerBindingCommitter } from './bot-routing-dispatch.mjs'
|
|
4
|
-
import { createBotTurnBackends } from './bot-routing-runtime.mjs'
|
|
5
|
-
import { createRoutineExecutionService } from './routine-execution.mjs'
|
|
6
|
-
import { createMachineHeartbeat } from './machine-heartbeat.mjs'
|
|
7
|
-
import { pairMainMachine } from './pair-main-machine.mjs'
|
|
8
|
-
import { assertHandoffAllowed, buildHandoff, createHandoffLedger } from './bot-handoff.mjs'
|
|
9
|
-
import { listRoles } from './bot-roles.mjs'
|
|
10
|
-
import { hostname } from 'node:os'
|
|
11
|
-
import { join, dirname, isAbsolute } from 'node:path'
|
|
12
|
-
import { mkdir } from 'node:fs/promises'
|
|
13
|
-
import { createRequire } from 'node:module'
|
|
14
|
-
import { randomUUID } from 'node:crypto'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
},
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
'
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
turnBackends
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1
|
+
import { createBotRoutingService } from './bot-routing-service.mjs'
|
|
2
|
+
import { openBotRoutingSettings } from './bot-routing-settings.mjs'
|
|
3
|
+
import { createWorkerBindingCommitter } from './bot-routing-dispatch.mjs'
|
|
4
|
+
import { createBotTurnBackends } from './bot-routing-runtime.mjs'
|
|
5
|
+
import { createRoutineExecutionService } from './routine-execution.mjs'
|
|
6
|
+
import { createMachineHeartbeat } from './machine-heartbeat.mjs'
|
|
7
|
+
import { pairMainMachine } from './pair-main-machine.mjs'
|
|
8
|
+
import { assertHandoffAllowed, buildHandoff, createHandoffLedger } from './bot-handoff.mjs'
|
|
9
|
+
import { listRoles } from './bot-roles.mjs'
|
|
10
|
+
import { hostname } from 'node:os'
|
|
11
|
+
import { join, dirname, isAbsolute } from 'node:path'
|
|
12
|
+
import { mkdir } from 'node:fs/promises'
|
|
13
|
+
import { createRequire } from 'node:module'
|
|
14
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
15
|
+
import { normalizeConversationEngines, mountConversationEngines, validateClaudeProfileCredential } from '../../bot-engines/src/host-registrations.mjs'
|
|
16
|
+
import { projectHostLoopTextRequest, hostLoopRouteContext, hostSessionPersona } from '../../bot-engines/src/host-loop-request.mjs'
|
|
17
|
+
import { independentSystemMessage } from './independent-message-wire.mjs'
|
|
18
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
19
|
+
import { defaultStatePath, resolveStateHome } from '../../../.dsh-plugin/shared/router-state.mjs'
|
|
20
|
+
import { actualCost } from '../../../.dsh-plugin/shared/run-ledger.mjs'
|
|
21
|
+
import { reservationSpending } from '../../../.dsh-plugin/shared/budget-reservations.mjs'
|
|
22
|
+
import { openProductFunding } from './product-funding.mjs'
|
|
23
|
+
import { createAuthority } from '../../capability-broker/src/authority.mjs'
|
|
24
|
+
import { createPrincipalDirectory } from './principals.mjs'
|
|
25
|
+
import { createBotPolicy } from './bot-policy.mjs'
|
|
26
|
+
import { createBotProduct, isAgentLoopRequest } from '../../../modules/bot-runtime/index.mjs'
|
|
27
|
+
import { createSessionLoopFrameResolver } from './model-loop-frame.mjs'
|
|
28
|
+
import { createAuxiliaryModelStream } from './model-auxiliary-dispatch.mjs'
|
|
29
|
+
import { compileMainBotGrant } from './main-bot-grant.mjs'
|
|
30
|
+
import { openRemoteMachines } from './remote-machines.mjs'
|
|
31
|
+
import { openSecurePairingHost } from './secure-pairing-host.mjs'
|
|
32
|
+
import { createDesktopProxy } from './desktop-proxy.mjs'
|
|
33
|
+
import { createDesktopHttpCarrier } from './desktop-http-carrier.mjs'
|
|
34
|
+
import { guardMachineSecrets } from './machine-secrets.mjs'
|
|
35
|
+
import { checkMainRouteFunding, openBotSettings } from './bot-settings.mjs'
|
|
36
|
+
import { fundingForMainRoute } from './main-model-routes.mjs'
|
|
37
|
+
import { parseModelProfilesJson } from '../../../.dsh-plugin/shared/model-profiles.mjs'
|
|
38
|
+
import { openFundingAccounts } from './funding-accounts.mjs'
|
|
39
|
+
import { createModelAccounting } from './model-accounting.mjs'
|
|
40
|
+
import { createModelBoundResolver } from './model-request-bounds.mjs'
|
|
41
|
+
import { createModelFrameStore } from './model-prepared-generation.mjs'
|
|
42
|
+
import { createTaskService } from './task-service.mjs'
|
|
43
|
+
import { inspectProductTask, projectProductTask, readProductAttention } from './task-product.mjs'
|
|
44
|
+
import { createBotNotifications } from './bot-notifications.mjs'
|
|
45
|
+
import { createBotThreadHost } from './bot-thread-host.mjs'
|
|
46
|
+
import { createIndependentToolHost } from './independent-tool-host.mjs'
|
|
47
|
+
import { extendIndependentToolHost } from './independent-tool-families.mjs'
|
|
48
|
+
import { createNativeConversationTools, renderNativeRemoteResult } from './native-conversation-tools.mjs'
|
|
49
|
+
import { openNativeRemoteActions, nativeRemoteToolEntry } from './native-remote-actions.mjs'
|
|
50
|
+
import { createIndependentChatService } from './independent-chat-service.mjs'
|
|
51
|
+
import { createGrantRefreshQueue } from './grant-refresh-queue.mjs'
|
|
52
|
+
import { openWebPanelStore } from './web-panel-store.mjs'
|
|
53
|
+
import { createWebPanelActions } from './web-panel-actions.mjs'
|
|
54
|
+
import { createDataEngine } from './bot-data-engine.mjs'
|
|
55
|
+
import { createProductDataAccess } from './data-product-access.mjs'
|
|
56
|
+
import { inspectProductBinding } from './binding-product.mjs'
|
|
57
|
+
import { inspectProductBotProfile, saveProductBotProfile } from './bot-profile.mjs'
|
|
58
|
+
import { openUiHostNamespace } from './ui-host-namespace.mjs'
|
|
59
|
+
import { registerDataTools } from './bot-data-tools.mjs'
|
|
60
|
+
import { createNativeDataTools } from './native-data-tools.mjs'
|
|
61
|
+
import { createNativeActivityService } from './native-activity-service.mjs'
|
|
62
|
+
import { createTeamCoordination } from './team-bridge.mjs'
|
|
63
|
+
import { createNativeTeamTools } from './native-team-tools.mjs'
|
|
64
|
+
import { registerTeamTools } from './team-sdk-tools.mjs'
|
|
65
|
+
import { createDesktopActions } from './desktop-actions.mjs'
|
|
66
|
+
import { registerTaskTools } from './task-tools.mjs'
|
|
67
|
+
import { createMrReadBackend } from './mr-read-backend.mjs'
|
|
68
|
+
import { createCliReadBackend } from './cli-execution.mjs'
|
|
69
|
+
import { createRemoteWriteBackend } from './remote-execution.mjs'
|
|
70
|
+
import { createRemoteCommandBackend } from './remote-command-execution.mjs'
|
|
71
|
+
import { createRemoteCliReadBackend } from './remote-cli-execution.mjs'
|
|
72
|
+
import { canonicalTaskJson, parseRemoteTaskCli, taskHash } from './task-contract.mjs'
|
|
73
|
+
import { acquireMigrationShared } from './migration-fence.mjs'
|
|
74
|
+
import { assertMigrationStartupAllowed } from './migration-startup-gate.mjs'
|
|
75
|
+
import { registerRemoteComputerTools } from './remote-computer-tools.mjs'
|
|
76
|
+
|
|
77
|
+
// The default Bot grant exposes the complete router through the same policy.
|
|
78
|
+
// Exact model routes and side-effect approvals remain independently enforced.
|
|
79
|
+
export const PRODUCT_TOOLS = Object.freeze(['model/infer',
|
|
80
|
+
'tool/model_router_routes', 'tool/model_router_plan', 'tool/model_router_consult', 'tool/model_router_health', 'tool/model_router_tools',
|
|
81
|
+
'tool/model_router_execute', 'tool/model_router_team_execute', 'tool/model_router_tool_run', 'tool/model_router_rerun_step',
|
|
82
|
+
'tool/model_router_tool_install', 'tool/model_router_tool_repair', 'tool/model_router_tool_uninstall', 'tool/model_router_rate',
|
|
83
|
+
'command/router', 'tool/bot_library_list', 'tool/bot_library_read', 'tool/bot_library_write', 'tool/bot_library_search', 'tool/bot_route_task',
|
|
84
|
+
'tool/bot_routine_list', 'tool/bot_routine_save', 'tool/bot_routine_run'])
|
|
85
|
+
export const PRODUCT_OWNER_READ_METHODS = Object.freeze(['list','installSettings','status','health','ledger','boundaries','previewRun','runStatus','terminalInfo','terminalRead'])
|
|
86
|
+
export const PRODUCT_OWNER_WRITE_METHODS = Object.freeze(['completeOnboarding','rateResult','rerunStep','startRun','cancelRun','installTool','uninstallTool','repairTool','cancel','terminalStart','terminalWrite','terminalResize','terminalStop'])
|
|
87
|
+
// The Bot runtime reports descriptive call origins; the durable receipt schema
|
|
88
|
+
// keeps its closed vocabulary. The adapter is explicit: an unmapped origin is
|
|
89
|
+
// refused by the accounting layer instead of being invented.
|
|
90
|
+
const RECEIPT_SOURCE = Object.freeze({ 'bot-session': 'bot', 'bot-auxiliary': 'aux' })
|
|
91
|
+
const ROUTE_REFRESH_MS = 30_000
|
|
92
|
+
const ROUTE_REFRESH_DEBOUNCE_MS = 2_000
|
|
93
|
+
const INITIAL_ROUTE_TIMEOUT_MS = 5_000
|
|
94
|
+
const OBSERVATION_LIMIT = 256
|
|
95
|
+
const validPolicy = value => Boolean(value) && ['run', 'check', 'filterRoutes'].every(key => typeof value[key] === 'function')
|
|
96
|
+
// N09: one managed Task runner over the same SQLite authority. A crashed Host
|
|
97
|
+
// cannot release its runner lease, so the next boot waits for it to expire
|
|
98
|
+
// instead of silently dropping every managed task.
|
|
99
|
+
const TASK_RUNNER_TTL_MS = 10_000
|
|
100
|
+
const TASK_RUNNER_CLAIM_MS = 12_000
|
|
101
|
+
// Frequent enough that a stopped runner is rebuilt inside one lease period,
|
|
102
|
+
// rare enough that a healthy Host only reads one row every five seconds.
|
|
103
|
+
const TASK_RUNNER_WATCHDOG_MS = 5_000
|
|
104
|
+
const TASK_TTL_MS = 120_000
|
|
105
|
+
const TASK_BACKEND_LIMIT = 64
|
|
106
|
+
const TASK_READ_LIMIT = 512
|
|
107
|
+
const CLI_DEADLINE_MS = 120_000
|
|
108
|
+
const CLI_MODEL = /^gpt-[A-Za-z0-9_.-]+$/
|
|
109
|
+
const CLI_ENTRY_VERSION = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/
|
|
110
|
+
const TASK_TOOLS = Object.freeze(['task/execute', 'task/submit', 'task/read', 'task/cancel', 'task/retry', 'task/reconcile'])
|
|
111
|
+
const TASK_CAPABILITY = Object.freeze({ id: 'managed-tasks', label: '托管任务',
|
|
112
|
+
tools: Object.freeze([...TASK_TOOLS, 'execution/mr-api', 'execution/mr-retry', 'execution/mr-observe',
|
|
113
|
+
'execution/bot-turn', 'execution/bot-turn-retry', 'execution/bot-turn-observe']) })
|
|
114
|
+
const CLI_GRANT_TOOLS = Object.freeze(['cli/codex', 'cli/cancel', 'cli/read', 'cli/retry'])
|
|
115
|
+
const MR_BACKEND = Object.freeze({ id: 'mr-api', version: 'mr-read-product-v1',
|
|
116
|
+
capability: 'execution/mr-api', retryCapability: 'execution/mr-retry', reconcileCapability: 'execution/mr-observe' })
|
|
117
|
+
const CLI_BACKEND = Object.freeze({ id: 'codex-read', version: 'cli-read-product-v1',
|
|
118
|
+
capability: 'cli/codex', retryCapability: 'cli/retry', reconcileCapability: 'cli/read' })
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compile one Bot's authority from its committed configuration. A Bot whose
|
|
122
|
+
* exact provider/model is not a configured route keeps no model authority:
|
|
123
|
+
* the strict rule pauses it instead of silently switching models.
|
|
124
|
+
*/
|
|
125
|
+
export function compileProductGrant(bot, { routes, hydrated }) {
|
|
126
|
+
const key = typeof bot?.model === 'string' ? bot.model : ''
|
|
127
|
+
const slash = key.indexOf('/')
|
|
128
|
+
const provider = slash > 0 ? key.slice(0, slash) : ''
|
|
129
|
+
const model = slash > 0 ? key.slice(slash + 1) : ''
|
|
130
|
+
return { tools: [...PRODUCT_TOOLS],
|
|
131
|
+
modelRoutes: hydrated && provider && model && routes.has(key) ? [{ provider, model }] : [] }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An action that is only meaningful for a signed-in person says so at its own
|
|
136
|
+
* entry. The authorization seam decides who may call, but a route that forgets to
|
|
137
|
+
* declare its caller class inherits whatever the seam defaults to, and a later
|
|
138
|
+
* change there would hand a non-owner somebody's console.
|
|
139
|
+
*/
|
|
140
|
+
const ownerOnly = fn => (payload, context) => {
|
|
141
|
+
if (context?.kind !== 'owner') throw Object.assign(new Error('OWNER_ONLY'), { code: 'OWNER_ONLY' })
|
|
142
|
+
return fn(payload, context)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Subscription usage is not API spend; missing token evidence stays unknown. */
|
|
146
|
+
export function projectModelCallAmount(receipt, resolution = null) {
|
|
147
|
+
if (resolution) return resolution.costUsd
|
|
148
|
+
if (receipt.funding === 'subscription') return 0
|
|
149
|
+
return actualCost({ usage: receipt.usage, usageIncomplete: !receipt.usageComplete,
|
|
150
|
+
reportedCostUsd: receipt.reportedCostUsd }, receipt.pricing).costUsd
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const taskInput = input => {
|
|
154
|
+
const value = JSON.parse(canonicalTaskJson(input))
|
|
155
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1
|
|
156
|
+
|| typeof value.task !== 'string' || !value.task.trim() || Buffer.byteLength(value.task) > 2000) {
|
|
157
|
+
throw Object.assign(new Error('INVALID_TASK_INPUT'), { code: 'INVALID_TASK_INPUT' })
|
|
158
|
+
}
|
|
159
|
+
return value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Host-configured official CLI entry. The executable, digest and version come
|
|
164
|
+
* from installation verification (`pinCodexExecutable` re-pins them at probe
|
|
165
|
+
* time); neither a Bot nor a Task input can supply an entry.
|
|
166
|
+
*/
|
|
167
|
+
export function normalizeTaskCli(value) {
|
|
168
|
+
if (value === undefined || value === null) return null
|
|
169
|
+
const entry = value.entry
|
|
170
|
+
const remote = (Array.isArray(value.remote) ? value.remote : []).map(profile => {
|
|
171
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)
|
|
172
|
+
|| Object.keys(profile).some(key => !['machineId','rootId','profileId','profileRevision','entry'].includes(key))
|
|
173
|
+
|| !['machineId','rootId','profileId'].every(key => typeof profile[key] === 'string' && /^[-A-Za-z0-9_.]{1,128}$/.test(profile[key]))
|
|
174
|
+
|| !Number.isSafeInteger(profile.profileRevision) || profile.profileRevision < 1
|
|
175
|
+
|| !profile.entry || Object.keys(profile.entry).some(key => !['sha256','version'].includes(key))
|
|
176
|
+
|| !/^[a-f0-9]{64}$/.test(profile.entry.sha256 ?? '') || !CLI_ENTRY_VERSION.test(profile.entry.version ?? '')) throw new TypeError('TASK_CLI_REMOTE_PROFILE_INVALID')
|
|
177
|
+
return Object.freeze({ machineId: profile.machineId, rootId: profile.rootId, profileId: profile.profileId, profileRevision: profile.profileRevision,
|
|
178
|
+
entry: Object.freeze({ sha256: profile.entry.sha256, version: profile.entry.version }) })
|
|
179
|
+
})
|
|
180
|
+
if (new Set(remote.map(profile => `${profile.machineId}/${profile.rootId}`)).size !== remote.length) throw new TypeError('TASK_CLI_REMOTE_PROFILE_DUPLICATE')
|
|
181
|
+
if (!entry && !remote.length) throw new TypeError('TASK_CLI_ENTRY_REQUIRED')
|
|
182
|
+
if (entry && (typeof entry.executable !== 'string' || !isAbsolute(entry.executable)
|
|
183
|
+
|| typeof entry.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(entry.sha256)
|
|
184
|
+
|| typeof entry.version !== 'string' || !CLI_ENTRY_VERSION.test(entry.version))) throw new TypeError('TASK_CLI_ENTRY_REQUIRED')
|
|
185
|
+
const models = (Array.isArray(value.models) ? value.models : []).map(model => {
|
|
186
|
+
if (!model || typeof model.provider !== 'string' || typeof model.model !== 'string'
|
|
187
|
+
|| typeof model.cliModel !== 'string' || !CLI_MODEL.test(model.cliModel)) throw new TypeError('TASK_CLI_MODEL_REQUIRED')
|
|
188
|
+
return { provider: model.provider, model: model.model, cliModel: model.cliModel }
|
|
189
|
+
})
|
|
190
|
+
if (new Set(models.map(model => `${model.provider}/${model.model}/${model.cliModel}`)).size !== models.length) throw new TypeError('TASK_CLI_MODEL_DUPLICATE')
|
|
191
|
+
if (value.sessionFactory !== undefined && typeof value.sessionFactory !== 'string') throw new TypeError('TASK_CLI_SESSION_FACTORY_REQUIRED')
|
|
192
|
+
return Object.freeze({ entry: entry ? Object.freeze({ executable: entry.executable, sha256: entry.sha256, version: entry.version }) : null,
|
|
193
|
+
models: Object.freeze(models), remote: Object.freeze(remote), sessionFactory: value.sessionFactory ?? null })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Stable managed Task backends for the default product.
|
|
198
|
+
*
|
|
199
|
+
* The durable target contract (id, version, effect, location, capabilities)
|
|
200
|
+
* is fixed per backend kind, while the execution target, model and money are
|
|
201
|
+
* resolved at run time from the submitting Bot's committed binding. A binding
|
|
202
|
+
* change therefore never rewrites a persisted target hash: retries and
|
|
203
|
+
* reconciliations of older tasks stay admissible, and each Bot is charged at
|
|
204
|
+
* its own configured price. The Bot binding is the single source of truth for
|
|
205
|
+
* both the grant and the money, so no service-level model check is needed.
|
|
206
|
+
*/
|
|
207
|
+
export function createTaskBackends({ ctx, mr, authority, directory, store, machineId, workspaceId, workspace, runtimeRoot, bindingFor, cli = null }) {
|
|
208
|
+
if (typeof bindingFor !== 'function' || typeof directory?.forBot !== 'function') throw new TypeError('TASK_BACKEND_SERVICES_REQUIRED')
|
|
209
|
+
if (store?.kind !== 'sqlite-router-state') throw new TypeError('TASK_BACKEND_STORE_REQUIRED')
|
|
210
|
+
const inners = new Map()
|
|
211
|
+
const innerFor = (kind, botId, build) => {
|
|
212
|
+
const key = taskHash({ kind, botId, ...build.key })
|
|
213
|
+
const cached = inners.get(key)
|
|
214
|
+
if (cached) return cached
|
|
215
|
+
if (inners.size >= TASK_BACKEND_LIMIT) return null
|
|
216
|
+
const value = build.create()
|
|
217
|
+
inners.set(key, value)
|
|
218
|
+
return value
|
|
219
|
+
}
|
|
220
|
+
const mrInnerFor = botId => {
|
|
221
|
+
const binding = bindingFor(botId)
|
|
222
|
+
const settings = binding?.status === 'available' ? binding.settings : null
|
|
223
|
+
const funding = settings?.funding ?? null
|
|
224
|
+
if (!settings?.model?.provider || !settings.model.model || funding?.mode !== 'api'
|
|
225
|
+
|| !funding.pricing || !funding.request || funding.dailyBudgetUsd === null) return null
|
|
226
|
+
const model = { provider: settings.model.provider, model: settings.model.model }
|
|
227
|
+
const pricing = funding.pricing, dailyBudgetUsd = funding.dailyBudgetUsd
|
|
228
|
+
return innerFor('mr', botId, { key: { model, pricing, dailyBudgetUsd },
|
|
229
|
+
create: () => createMrReadBackend({ ctx, mr, model, workspace, machineId, workspaceId, pricing, dailyBudgetUsd }) })
|
|
230
|
+
}
|
|
231
|
+
const cliInnerFor = botId => {
|
|
232
|
+
if (!cli) return null
|
|
233
|
+
const binding = bindingFor(botId)
|
|
234
|
+
const settings = binding?.status === 'available' ? binding.settings : null
|
|
235
|
+
const funding = settings?.funding ?? null
|
|
236
|
+
const tool = settings?.officialTools?.find(item => item.toolId === 'codex' && item.mode === 'read-only')
|
|
237
|
+
if (!settings?.model?.provider || !settings.model.model || !tool || !funding?.scopeId) return null
|
|
238
|
+
if (funding.mode === 'api' && (!funding.pricing || !funding.request)) return null
|
|
239
|
+
if (funding.mode === 'subscription' && !(funding.subscription?.dailyCalls >= 1)) return null
|
|
240
|
+
const model = { provider: settings.model.provider, model: settings.model.model }
|
|
241
|
+
const billing = funding.mode
|
|
242
|
+
const pricing = billing === 'api' ? funding.pricing : null
|
|
243
|
+
const estimatedCostUsd = billing === 'api'
|
|
244
|
+
? (funding.request.maxInputTokens * pricing.input + funding.request.maxOutputTokens * pricing.output) / 1e6 : null
|
|
245
|
+
const dailyLimitUsd = funding.dailyBudgetUsd ?? 0, monthlyLimitUsd = funding.monthlyBudgetUsd ?? 0
|
|
246
|
+
const subscriptionMaxCalls = billing === 'subscription' ? funding.subscription.dailyCalls : null
|
|
247
|
+
return innerFor('cli', botId, { key: { model, cliModel: tool.cliModel, billing, pricing, dailyLimitUsd, monthlyLimitUsd, subscriptionMaxCalls },
|
|
248
|
+
create: () => createCliReadBackend({ store, authority, directory, entry: cli.entry, runtimeRoot, workspace, machineId, workspaceId,
|
|
249
|
+
model, requestedModel: tool.cliModel, billing, pricing, estimatedCostUsd, dailyLimitUsd, monthlyLimitUsd,
|
|
250
|
+
subscriptionMaxCalls, deadlineMs: CLI_DEADLINE_MS, tools: [], handlers: [], sessionFactory: cli.sessionFactory }) })
|
|
251
|
+
}
|
|
252
|
+
const backend = (spec, resolve) => Object.freeze({
|
|
253
|
+
id: spec.id, version: spec.version, effect: 'read', machineId, workspaceId, location: 'local',atomicAdmission:true,
|
|
254
|
+
capability: spec.capability, retryCapability: spec.retryCapability, reconcileCapability: spec.reconcileCapability,
|
|
255
|
+
parse: taskInput,
|
|
256
|
+
run: context => { const inner = resolve(context.ownerBotId); return inner ? inner.run(context) : { outcome: 'failed', summary: { code: 'TASK_BINDING_UNAVAILABLE' } } },
|
|
257
|
+
reconcile: context => { const inner = resolve(context.ownerBotId); return inner ? inner.reconcile(context) : { outcome: 'unknown', reason: 'TASK_BINDING_UNAVAILABLE' } },
|
|
258
|
+
})
|
|
259
|
+
const backends = [backend(MR_BACKEND, mrInnerFor)]
|
|
260
|
+
if (cli) backends.push(backend(CLI_BACKEND, cliInnerFor))
|
|
261
|
+
return Object.freeze({ backends: Object.freeze(backends),
|
|
262
|
+
catalog: () => backends.map((item, index) => ({ id: item.id, version: item.version, effect: item.effect, location: item.location,
|
|
263
|
+
capability: item.capability, label: index === 0 ? '模型路由只读任务' : '官方 CLI 只读任务' })) })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createRemoteCliTaskBackend({machines,directory,store,authority,profiles,bindingFor}){
|
|
267
|
+
const inners=new Map()
|
|
268
|
+
const resolve=botId=>{
|
|
269
|
+
const binding=bindingFor(botId),settings=binding?.status==='available'?binding.settings:null,funding=settings?.funding??null
|
|
270
|
+
const tool=settings?.officialTools?.find(item=>item.toolId==='codex'&&item.mode==='read-only')
|
|
271
|
+
if(!settings?.model?.provider||!settings.model.model||!tool||!funding?.scopeId)return null
|
|
272
|
+
if(funding.mode==='api'&&(!funding.pricing||!funding.request))return null
|
|
273
|
+
if(funding.mode==='subscription'&&!(funding.subscription?.dailyCalls>=1))return null
|
|
274
|
+
const model={provider:settings.model.provider,model:settings.model.model},billing=funding.mode,pricing=billing==='api'?funding.pricing:null
|
|
275
|
+
const estimatedCostUsd=billing==='api'?(funding.request.maxInputTokens*pricing.input+funding.request.maxOutputTokens*pricing.output)/1e6:null
|
|
276
|
+
const dailyLimitUsd=funding.dailyBudgetUsd??0,monthlyLimitUsd=funding.monthlyBudgetUsd??0,subscriptionMaxCalls=billing==='subscription'?funding.subscription.dailyCalls:null
|
|
277
|
+
const key=taskHash({botId,model,cliModel:tool.cliModel,billing,pricing,dailyLimitUsd,monthlyLimitUsd,subscriptionMaxCalls})
|
|
278
|
+
if(inners.has(key))return inners.get(key)
|
|
279
|
+
if(inners.size>=TASK_BACKEND_LIMIT)return null
|
|
280
|
+
const inner=createRemoteCliReadBackend({machines,directory,store,authority,profiles,model,requestedModel:tool.cliModel,billing,pricing,estimatedCostUsd,dailyLimitUsd,monthlyLimitUsd,subscriptionMaxCalls,deadlineMs:CLI_DEADLINE_MS})
|
|
281
|
+
inners.set(key,inner);return inner
|
|
282
|
+
}
|
|
283
|
+
const forSubject=subject=>resolve(authority.describe(subject).botId)
|
|
284
|
+
const unavailable=()=>{throw Object.assign(new Error('TASK_BINDING_UNAVAILABLE'),{code:'TASK_BINDING_UNAVAILABLE'})}
|
|
285
|
+
return Object.freeze({id:'remote-codex-read',version:'remote-cli-read-product-v1',effect:'read',location:'remote',atomicAdmission:true,requiresExecutionInput:true,
|
|
286
|
+
machineId:'remote-target',workspaceId:'remote-root',capability:'remote/cli_task_v1',retryCapability:'remote/cli_task_v1',reconcileCapability:'remote/operation_status',parse:parseRemoteTaskCli,
|
|
287
|
+
targetFor(subject,input){return (forSubject(subject)??unavailable()).targetFor(subject,input)},
|
|
288
|
+
canObserve(subject,target,input){return forSubject(subject)?.canObserve(subject,target,input)??false},
|
|
289
|
+
run(context){const inner=resolve(context.ownerBotId);return inner?inner.run(context):{outcome:'failed',summary:{code:'TASK_BINDING_UNAVAILABLE'}}},
|
|
290
|
+
reconcile(context){const inner=resolve(context.ownerBotId);return inner?inner.reconcile(context):{outcome:'unknown',reason:'TASK_BINDING_UNAVAILABLE'}},
|
|
291
|
+
probe(subject,input){const inner=forSubject(subject);if(!inner)unavailable();return inner.probe(subject,input)},
|
|
292
|
+
async close(){await Promise.allSettled([...inners.values()].map(inner=>inner.close()))},
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Compose the frozen DS Bot runtime with the MR plugin body inside one Host.
|
|
298
|
+
* The synchronous segment creates the shared authority/directory/policy and
|
|
299
|
+
* provides `botIntegrationPolicy` before this function first returns, so every
|
|
300
|
+
* legacy MR tool, command and RPC receiver resolves the same policy at call
|
|
301
|
+
* time. The DS runtime itself is created after a bounded initial route
|
|
302
|
+
* snapshot, because grant compilation is synchronous.
|
|
303
|
+
*
|
|
304
|
+
* When another trusted Host component already provides a valid
|
|
305
|
+
* `botIntegrationPolicy` (isolated test fixtures), this composition defers to
|
|
306
|
+
* it instead of installing a second runtime or a duplicate service.
|
|
307
|
+
*
|
|
308
|
+
* `accountStream` is the shared money decorator. M1 passes a clearly labeled
|
|
309
|
+
* diagnostic decorator: it records bounded identity/source observations and
|
|
310
|
+
* never claims a monetary amount. Per-source receipts and budget admission
|
|
311
|
+
* arrive with the unified SQLite money authority.
|
|
312
|
+
*/
|
|
313
|
+
export function startBotProduct(ctx, options = {}) {
|
|
314
|
+
const { discoverRoutes, accountStream, config = {}, actions = {}, machineId = `local-${hostname()}`, workspaceId = 'default',
|
|
315
|
+
runtimeConfig, onBind, refreshIntervalMs = ROUTE_REFRESH_MS, now = Date.now, failClosed = true,
|
|
316
|
+
deferredRouterState = null, legacyFile = defaultStatePath(), pricingFor = async () => null, mr = null, taskCli = null,
|
|
317
|
+
Database = createRequire(import.meta.url)('better-sqlite3'), storageFile = join(resolveStateHome(), 'model-router', 'orchestration.sqlite') } = options
|
|
318
|
+
if (discoverRoutes !== undefined && typeof discoverRoutes !== 'function') throw new TypeError('BOT_PRODUCT_ROUTES_REQUIRED')
|
|
319
|
+
if (typeof accountStream !== 'function') throw new TypeError('BOT_PRODUCT_ACCOUNTING_REQUIRED')
|
|
320
|
+
// A misconfigured official CLI entry must fail loudly at composition time
|
|
321
|
+
// instead of silently degrading the managed Task surface.
|
|
322
|
+
const cliConfig = normalizeTaskCli(taskCli)
|
|
323
|
+
const engineProfiles = normalizeConversationEngines(options.conversationEngines ?? [])
|
|
324
|
+
// Grant compilation needs the raw configured-route catalog. The plugin's
|
|
325
|
+
// discoverConfiguredRoutes applies the per-subject policy filter, which
|
|
326
|
+
// refuses outside an execution context; a filtered catalog would leave every
|
|
327
|
+
// Bot without model authority.
|
|
328
|
+
const catalog = async () => {
|
|
329
|
+
if (typeof discoverRoutes === 'function') return discoverRoutes()
|
|
330
|
+
const providers = ctx.llm?.listProviders?.() ?? []
|
|
331
|
+
const routes = []
|
|
332
|
+
for (const entry of Array.isArray(providers) ? providers : []) {
|
|
333
|
+
const provider = typeof entry === 'string' ? entry : entry?.id
|
|
334
|
+
if (!provider) continue
|
|
335
|
+
let models
|
|
336
|
+
try { models = await ctx.llm.listModels(provider) } catch { continue }
|
|
337
|
+
for (const listed of Array.isArray(models) ? models : []) {
|
|
338
|
+
const model = listed?.id ?? listed?.model
|
|
339
|
+
if (model) routes.push({ provider, model })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return routes
|
|
343
|
+
}
|
|
344
|
+
let provided
|
|
345
|
+
try { provided = ctx.get?.('botIntegrationPolicy') } catch { /* absent optional service */ }
|
|
346
|
+
if (provided !== undefined) {
|
|
347
|
+
if (!validPolicy(provided)) throw new Error('BOT_PRODUCT_POLICY_CONFLICT: an invalid botIntegrationPolicy is already provided')
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
const authority = createAuthority()
|
|
351
|
+
const directory = createPrincipalDirectory(authority)
|
|
352
|
+
ctx.provide('botIdForAgent', agent => {
|
|
353
|
+
try { return authority.describe(directory.forAgent(agent)).botId } catch { return null }
|
|
354
|
+
})
|
|
355
|
+
const ownerTools=[...PRODUCT_OWNER_READ_METHODS,...PRODUCT_OWNER_WRITE_METHODS].map(name=>`remote/${name}`)
|
|
356
|
+
const policy = createBotPolicy({ authority, directory, machineId, workspaceId,ownerGrant:{tools:ownerTools} })
|
|
357
|
+
ctx.provide('botIntegrationPolicy', policy)
|
|
358
|
+
// Bot routing reads the same committed state everything else obeys: the Bot list, each Bot's
|
|
359
|
+
// saved settings, and machine reachability from health. It never invents a candidate.
|
|
360
|
+
// Routines live beside the router state; the plugin owns that directory and provides the store.
|
|
361
|
+
// Handing work between Bots: the ledger makes loops impossible, the plan carries the sender.
|
|
362
|
+
const handoffLedger = createHandoffLedger()
|
|
363
|
+
const handoffTask = async (args, context) => {
|
|
364
|
+
if (context?.kind !== 'bot' || context.botId !== args.fromBotId
|
|
365
|
+
|| !product?.lifecycle.committedMainBotIds().includes(context.botId)) throw new Error('BOT_ROUTING_HANDOFF_DENIED')
|
|
366
|
+
assertHandoffAllowed({ taskId: args.taskId, fromBotId: args.fromBotId, toBotId: args.toBotId, ledger: handoffLedger })
|
|
367
|
+
const { plan } = buildHandoff({ taskId: args.taskId, fromBotId: args.fromBotId, toBotId: args.toBotId, objective: args.objective })
|
|
368
|
+
if (!taskService) throw new Error('TASK_SERVICE_UNAVAILABLE')
|
|
369
|
+
plan.origin.sessionId = context.sessionId
|
|
370
|
+
for (const item of plan.packages) item.execution = { backend: 'bot-turn-read', input: { task: item.objective } }
|
|
371
|
+
const submitted = await taskService.submitPlan(taskOwner({ botId: args.fromBotId }), { requestKey: `handoff-${args.taskId}-${args.toBotId}`, plan }, { sessionId: context.sessionId })
|
|
372
|
+
turnBackends?.watchPlan({ planId: submitted.planId, botId: args.toBotId, originBotId: args.fromBotId })
|
|
373
|
+
handoffLedger.held(args.taskId, args.fromBotId)
|
|
374
|
+
handoffLedger.held(args.taskId, args.toBotId)
|
|
375
|
+
return { plan, submitted }
|
|
376
|
+
}
|
|
377
|
+
const routingSettingsFor = botId => {
|
|
378
|
+
try {
|
|
379
|
+
const row = botSettings?.get(botId, { publishChange: false })
|
|
380
|
+
if(!row)return null
|
|
381
|
+
const main=product?.lifecycle.committedMainBotIds().includes(botId)===true
|
|
382
|
+
const scopes=main?(machines?.scopes(botId)??[]):[]
|
|
383
|
+
const remoteIds=new Set((machines?.readModel().items??[]).map(machine=>machine.id))
|
|
384
|
+
const machineGrants=main?[...row.settings.machineGrants.filter(grant=>!remoteIds.has(grant.machineId)),
|
|
385
|
+
...scopes.flatMap(scope=>scope.roots.map(rootId=>({machineId:scope.machineId,rootId,mode:scope.mode,capabilities:[...scope.tools]})))]:row.settings.machineGrants
|
|
386
|
+
return {...row.settings,machineGrants,grant:main?(product?.lifecycle.committedGrant(botId)??row.grant):row.grant,status:row.status}
|
|
387
|
+
}
|
|
388
|
+
catch { return null }
|
|
389
|
+
}
|
|
390
|
+
const commitWorkerBinding = createWorkerBindingCommitter({ machines: () => machines, settingsFor: routingSettingsFor,
|
|
391
|
+
mainBotIds: () => product?.lifecycle.committedMainBotIds() ?? [] })
|
|
392
|
+
const botRouting = createBotRoutingService({
|
|
393
|
+
bots: () => (product?.lifecycle.committedBots() ?? []).map(bot=>({...bot,main:product.lifecycle.committedMainBotIds().includes(bot.id)})),
|
|
394
|
+
settingsFor: routingSettingsFor,
|
|
395
|
+
machineHealth: machineId => {
|
|
396
|
+
try { const row = machines?.readModel?.().items?.find(item => item.id === machineId); return { online: row?.status === 'online' } } catch { return null }
|
|
397
|
+
},
|
|
398
|
+
routing: () => routingSettings?.get().settings ?? config.botRouting,
|
|
399
|
+
mainBotIds: () => product?.lifecycle.committedMainBotIds() ?? [],
|
|
400
|
+
subjectFor: botId => directory.forBot(botId),
|
|
401
|
+
submitPlan: (subject, request, caller) => {
|
|
402
|
+
if (!taskService) throw new Error('TASK_SERVICE_UNAVAILABLE')
|
|
403
|
+
return taskService.submitPlan(subject, request, caller)
|
|
404
|
+
},
|
|
405
|
+
readSubmittedPlan: (originBotId, planId) => {
|
|
406
|
+
if (!journal) throw Object.assign(new Error('BOT_ROUTING_STATUS_UNAVAILABLE'), { code: 'BOT_ROUTING_STATUS_UNAVAILABLE' })
|
|
407
|
+
// Authenticated owner observation reads durable identities even if the Bot was paused.
|
|
408
|
+
return journal.getPlan(originBotId, planId)
|
|
409
|
+
},
|
|
410
|
+
bindWorker: (binding, context) => commitWorkerBinding(binding, context),
|
|
411
|
+
executionBackendFor: effect => effect === 'read' ? 'bot-turn-read' : 'bot-turn-write',
|
|
412
|
+
onAccepted: report => turnBackends?.watchPlan(report),
|
|
413
|
+
busyFor: botId => product?.rt.live?.get(product.rt.chatOf(botId))?.turn !== undefined,
|
|
414
|
+
priceFor: (route, botId) => {
|
|
415
|
+
// Funding prices belong to the Bot binding, even when another Bot uses the same model.
|
|
416
|
+
const settings = routingSettingsFor(botId)
|
|
417
|
+
if (settings?.model?.provider !== route.provider || settings.model.model !== route.model) return null
|
|
418
|
+
const price = settings.funding?.pricing
|
|
419
|
+
if (!Number.isFinite(price?.input) || price.input < 0 || !Number.isFinite(price?.output) || price.output < 0) return null
|
|
420
|
+
return (price.input + price.output) / 2
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
ctx.provide('botRouting', botRouting)
|
|
424
|
+
const describeBot = subject => { try { return authority.describe(subject).botId } catch { return null } }
|
|
425
|
+
// Operator passthrough: a session that is not a Bot (the operator's own agent session) would
|
|
426
|
+
// otherwise be refused as UNKNOWN_SUBJECT by every Bot-shaped tool. When the operator turns this
|
|
427
|
+
// on, such a session acts with the Main Bot's authority — the same grants, the same audit trail
|
|
428
|
+
// — instead of being handed a second, parallel set of permissions.
|
|
429
|
+
const operatorSubject = () => {
|
|
430
|
+
const settings = config?.operatorPassthrough
|
|
431
|
+
// On by default: the operator's own session is the person who owns the machines.
|
|
432
|
+
if (settings && settings.enabled === false) return null
|
|
433
|
+
const mainId = product?.lifecycle.committedMainBotIds()[0]
|
|
434
|
+
if (!mainId) return null
|
|
435
|
+
try { return directory.forBot(mainId) ?? null } catch { return null }
|
|
436
|
+
}
|
|
437
|
+
const subjectOrOperator = subject => subject ?? operatorSubject()
|
|
438
|
+
ctx.provide('operatorSubject', operatorSubject)
|
|
439
|
+
// Owner task management acts on the owning Bot's own subject: the Bot's
|
|
440
|
+
// committed grant authorizes the operation, never the browser session.
|
|
441
|
+
const taskOwner = payload => {
|
|
442
|
+
const botId = payload?.botId
|
|
443
|
+
if (typeof botId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(botId)) throw new Error('INVALID_TASK_OWNER')
|
|
444
|
+
return directory.forBot(botId)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const grants = new Map()
|
|
448
|
+
const modelFrames = createModelFrameStore()
|
|
449
|
+
const loopFunding = new WeakMap()
|
|
450
|
+
const sessionLoopFrame = createSessionLoopFrameResolver({ sessionFor: id => ctx.get('sessions')?.get?.(id), isLoopRequest: isAgentLoopRequest,
|
|
451
|
+
allowUnbounded: request => engineProfiles.some(profile => profile.provider === request.provider && profile.model === request.model) })
|
|
452
|
+
let conversationEngines
|
|
453
|
+
const engineCredentialStamps = new Map()
|
|
454
|
+
const engineLeaseId = `native-${randomUUID()}`
|
|
455
|
+
let machines, nativeRemoteActions, product, releaseMachineSecrets, funding, botSettings, fundingAccounts, modelAccounting, routingSettings, turnBackends, routineExecution, machineHeartbeat, securePairing, desktopProxy, desktopHttp
|
|
456
|
+
let journal, taskService, taskBackends, releaseTaskTools, cliSessionFactory, remoteCliBackend, sharedAdmission, taskRunnerWatchdog, notifications, dataEngine, dataAccess, releaseDataTools, uiNamespace, nativeActivity, botThreads, webPanelStore, webPanelActions, independentChat, teamCoordination, releaseTeamTools, releaseTeamGuard, releaseTeamStop
|
|
457
|
+
const grantRefresh = createGrantRefreshQueue({ lifecycle: () => product?.lifecycle,
|
|
458
|
+
onError: () => ctx.logger?.warn?.('Bot grant refresh failed; grants remain suspended until a successful committed-state refresh.') })
|
|
459
|
+
// The service resolves live dependencies at each call. It is available before
|
|
460
|
+
// async boot, so connect cannot capture an absent tool service forever.
|
|
461
|
+
const ordinaryIndependentTools = createIndependentToolHost({ directory, authority,
|
|
462
|
+
machines: () => machines, taskService: () => taskService,
|
|
463
|
+
supportsToolsFor: (botId, nativeRoute) => nativeRoute
|
|
464
|
+
? engineProfiles.some(profile => profile.hostTools && profile.provider === nativeRoute.provider && profile.model === nativeRoute.model)
|
|
465
|
+
: !engineProfiles.some(profile => {
|
|
466
|
+
const bot=product?.lifecycle.committedBot(botId)
|
|
467
|
+
return bot?.model===`${profile.provider}/${profile.model}`
|
|
468
|
+
}),
|
|
469
|
+
backends: () => taskBackends ? taskBackends.catalog().map(entry => entry.id) : [], now })
|
|
470
|
+
ctx.provide('botIndependentToolHost', extendIndependentToolHost({ base: ordinaryIndependentTools,
|
|
471
|
+
validateCall: (botId, context) => teamCoordination?.assertCanAct(botId, { conversationId: context.conversationId }),
|
|
472
|
+
familiesFor: (botId, context) => {
|
|
473
|
+
const bot = product?.lifecycle.committedBot(botId)
|
|
474
|
+
if (!bot || !dataAccess || typeof bot.model !== 'string') return []
|
|
475
|
+
const modelRef = bot.model, slash = modelRef.indexOf('/')
|
|
476
|
+
if (slash < 1) return []
|
|
477
|
+
const route = { provider: modelRef.slice(0, slash), model: modelRef.slice(slash + 1) }
|
|
478
|
+
if (engineProfiles.some(profile => profile.provider === route.provider && profile.model === route.model)) return []
|
|
479
|
+
const bindingStamp = () => {
|
|
480
|
+
const binding = bindingRoute({ botId, ...route })
|
|
481
|
+
return binding ? JSON.stringify([binding.revision, binding.account?.revision ?? null]) : null
|
|
482
|
+
}
|
|
483
|
+
const subject = directory.forBot(botId), revision = bindingStamp()
|
|
484
|
+
if (!revision) return []
|
|
485
|
+
const validate = () => {
|
|
486
|
+
if (product.lifecycle.committedBot(botId)?.model !== modelRef || directory.forBot(botId) !== subject) throw Object.assign(new Error('Bot 配置已变化,本次工具结果暂不释放。'), { code: 'SUBJECT_MISMATCH' })
|
|
487
|
+
product.serving.assertMode(botId, 'independent')
|
|
488
|
+
authority.assertTool(subject, 'model/infer')
|
|
489
|
+
authority.assertModel(subject, route.provider, route.model)
|
|
490
|
+
if (bindingStamp() !== revision) throw Object.assign(new Error('模型资金配置已变化。'), { code: 'MODEL_FUNDING_CHANGED' })
|
|
491
|
+
}
|
|
492
|
+
try { validate() } catch { return [] }
|
|
493
|
+
const data = createNativeDataTools({ access: dataAccess, subject, botId, validate,
|
|
494
|
+
redact: product.rt.redactSecrets })
|
|
495
|
+
const entries = data.handlers.map(handler => ({ handler, validate, readonly: handler.definition.name === 'data_describe' }))
|
|
496
|
+
if (teamCoordination?.canUse(botId)) {
|
|
497
|
+
const conversationId = context?.conversationId ?? policy.activeIdentity()?.conversationId ?? ctx.get('botRuntimeAdapter')?.contextOf?.(botId)?.contextId
|
|
498
|
+
const team = createNativeTeamTools({ service: teamCoordination, callerBotId: botId, conversationId,
|
|
499
|
+
validate: () => { validate(); teamCoordination.assertCanAct(botId, { conversationId }) }, redact: product.rt.redactSecrets })
|
|
500
|
+
entries.push(...team.handlers.map(handler => ({ handler, validate, readonly: handler.definition.name !== 'coordinate_bots' })))
|
|
501
|
+
}
|
|
502
|
+
return entries
|
|
503
|
+
} }))
|
|
504
|
+
const threadAction = (name, payload) => {
|
|
505
|
+
if (!botThreads?.ready) throw Object.assign(new Error('当前宿主未提供完整线程会话能力。'), { code: 'BOT_THREADS_UNAVAILABLE' })
|
|
506
|
+
return botThreads.actions[name](payload)
|
|
507
|
+
}
|
|
508
|
+
const independentChatAction = (name, payload, context) => {
|
|
509
|
+
if (!independentChat?.ready || typeof independentChat[name] !== 'function') throw Object.assign(new Error('独立聊天尚未就绪。'), { code: 'INDEPENDENT_CHAT_UNAVAILABLE' })
|
|
510
|
+
const value = independentChat[name](payload, context)
|
|
511
|
+
if (['accepted', 'running'].includes(value?.state) && (name === 'newConversation' || name === 'cancel' && value.cancelRequested)) teamCoordination?.stop(payload.botId)
|
|
512
|
+
return value
|
|
513
|
+
}
|
|
514
|
+
const committedBots = new Map()
|
|
515
|
+
let ownerCatalogStamp=''
|
|
516
|
+
let routes = new Set(), hydrated = false, refreshing = false, lastRefresh = 0, timer = null
|
|
517
|
+
const refresh = async () => {
|
|
518
|
+
if (refreshing) return
|
|
519
|
+
refreshing = true
|
|
520
|
+
try {
|
|
521
|
+
routes = new Set((await catalog()).map(route => `${route.provider}/${route.model}`)); hydrated = true
|
|
522
|
+
const ownerRoutes=[...routes].sort().map(key=>{const slash=key.indexOf('/');return {provider:key.slice(0,slash),model:key.slice(slash+1)}}).filter(route=>route.provider&&route.model)
|
|
523
|
+
const stamp=JSON.stringify(ownerRoutes)
|
|
524
|
+
if(stamp!==ownerCatalogStamp){policy.replaceOwnerGrant({tools:ownerTools,modelRoutes:ownerRoutes});ownerCatalogStamp=stamp}
|
|
525
|
+
if(product)await product.lifecycle.refreshGrants()
|
|
526
|
+
}
|
|
527
|
+
catch { /* keep the last snapshot; the interval retries */ }
|
|
528
|
+
finally { refreshing = false; lastRefresh = now() }
|
|
529
|
+
}
|
|
530
|
+
const scheduleRefresh = () => {
|
|
531
|
+
if (refreshing || now() - lastRefresh < ROUTE_REFRESH_DEBOUNCE_MS) return
|
|
532
|
+
void refresh()
|
|
533
|
+
}
|
|
534
|
+
const grantFor = (bot, { main = false } = {}) => {
|
|
535
|
+
// The DS lifecycle compiles grants from committed state; recording here keeps
|
|
536
|
+
// bot-settings' botExists honest during that same synchronous compilation.
|
|
537
|
+
committedBots.set(bot.id, bot)
|
|
538
|
+
scheduleRefresh()
|
|
539
|
+
let grant = botSettings ? botSettings.grantFor(bot) : compileProductGrant(bot, { routes, hydrated })
|
|
540
|
+
// Main is full-control, compiled only from the saved role and Host catalogs.
|
|
541
|
+
// A worker's checkbox grant cannot silently narrow the owner's Main Bot.
|
|
542
|
+
if (main) grant = compileMainBotGrant({ base: grant, catalog: settingsCatalog(),
|
|
543
|
+
routes: hydrated ? [...routes.keys()].map(key => { const at = key.indexOf('/'); return { provider: key.slice(0, at), model: key.slice(at + 1) } }) : [],
|
|
544
|
+
toolNames: ctx.tools.schemas().map(tool => tool.name), teamToolNames: (product?.rt.teamTools ?? []).map(tool => tool.name) })
|
|
545
|
+
const names = {machine_ping:'remote_computer_ping',fs_list:'remote_computer_list',fs_read:'remote_computer_read',fs_write:'remote_computer_write'}
|
|
546
|
+
// Grant compilation is synchronous and runs on every DS save/load: an
|
|
547
|
+
// unavailable machine directory (not yet opened, or closed after a failed
|
|
548
|
+
// boot) grants no machine access instead of suspending the lifecycle.
|
|
549
|
+
let scopes = [],cliOfficial=false
|
|
550
|
+
// Authorisation comes from committed state; `scopes()` only adds capabilities that are live
|
|
551
|
+
// right now. Relying on scopes() alone meant a restart granted the Main Bot no remote tools
|
|
552
|
+
// at all, because the directory is not open yet when grants compile.
|
|
553
|
+
try { scopes = machines?.bindingsFor(bot.id,{main}) ?? [] } catch { scopes = [] }
|
|
554
|
+
cliOfficial=main
|
|
555
|
+
if(!main)try { cliOfficial=botSettings?.get(bot.id,{publishChange:false}).settings?.officialTools?.some(item=>item.toolId==='codex'&&item.mode==='read-only')===true } catch {}
|
|
556
|
+
for (const scope of scopes) for (const tool of scope.tools) {
|
|
557
|
+
if(['operation_status','operation_cancel'].includes(tool)){grant.tools.push(`remote/${tool}`,'tool/remote_computer_operation');continue}
|
|
558
|
+
if(tool==='artifact_stat'||tool==='artifact_read'){grant.tools.push(`remote/${tool}`);continue}
|
|
559
|
+
if(['cli_probe_v1','cli_task_v1'].includes(tool)){
|
|
560
|
+
const profile=cliConfig?.remote.find(item=>item.machineId===scope.machineId&&scope.roots.includes(item.rootId))
|
|
561
|
+
if(cliOfficial&&profile&&['cli_probe_v1','cli_task_v1','operation_status','operation_cancel'].every(name=>scope.tools.includes(name)))grant.tools.push(`remote/${tool}`,...(tool==='cli_probe_v1'?['tool/remote_computer_cli_probe']:[]))
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
if(tool.startsWith('browser_')){if(scope.mode==='workspace-write')grant.tools.push(`remote/${tool}`,'tool/remote_computer_browser');continue}
|
|
565
|
+
if(tool.startsWith('desktop_')){
|
|
566
|
+
if(scope.mode==='workspace-write'&&['artifact_stat','artifact_read','operation_status','operation_cancel'].every(name=>scope.tools.includes(name)))grant.tools.push(`remote/${tool}`,'tool/remote_computer_desktop')
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
if(tool==='container_command_v1'){
|
|
570
|
+
if(scope.mode==='workspace-write'&&['container_command_v1','operation_status','operation_cancel'].every(name=>scope.tools.includes(name)))grant.tools.push(`remote/${tool}`,'tool/remote_computer_command')
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
if (!names[tool]) continue
|
|
574
|
+
if(tool==='fs_write'&&(scope.mode!=='workspace-write'||!['fs_write','operation_status','operation_cancel'].every(name=>scope.tools.includes(name))))continue
|
|
575
|
+
grant.tools.push(`remote/${tool}`, `tool/${names[tool]}`)
|
|
576
|
+
}
|
|
577
|
+
grant.tools = [...new Set(grant.tools)]
|
|
578
|
+
grants.set(bot.id, grant)
|
|
579
|
+
return grant
|
|
580
|
+
}
|
|
581
|
+
const bindings = async ({ runtime, signal }) => {
|
|
582
|
+
signal?.throwIfAborted()
|
|
583
|
+
await runtime.load()
|
|
584
|
+
const state = runtime.state ?? {}
|
|
585
|
+
const items = (product?.lifecycle.committedBots()??[]).map(bot => {
|
|
586
|
+
const grant = grants.get(bot.id)
|
|
587
|
+
return { botId: bot.id, name: bot.name ?? '', role: bot.role ?? '', model: bot.model ?? null,
|
|
588
|
+
sessionId: runtime.chatOf(bot.id), main: product.lifecycle.committedMainBotIds().includes(bot.id),
|
|
589
|
+
tools: grant?.tools ?? [], modelRoutes: grant?.modelRoutes ?? [], workspaceId, machineId }
|
|
590
|
+
})
|
|
591
|
+
return { status: 'available', items }
|
|
592
|
+
}
|
|
593
|
+
// The parent Host authenticates the browser session; absence never enables a route.
|
|
594
|
+
const authorizeRequest = async request => {
|
|
595
|
+
const admission = ctx.connection?.admit(request)
|
|
596
|
+
if (!admission || 'rejection' in admission || admission.peer?.principal?.kind !== 'owner') return null
|
|
597
|
+
return { kind: 'owner', peer: admission.peer }
|
|
598
|
+
}
|
|
599
|
+
// Trusted catalog for Bot binding validation: exact route models, the local
|
|
600
|
+
// machine entry (an enabled binding requires a workspaceRef) and enabled
|
|
601
|
+
// funding scopes. Tools and capability catalogs arrive with their facades.
|
|
602
|
+
const settingsCatalog = () => {
|
|
603
|
+
const models = [...routes].map(key => { const slash = key.indexOf('/'); return { provider: key.slice(0, slash), model: key.slice(slash + 1), label: key, cliModels: [] } })
|
|
604
|
+
for (const model of models) {
|
|
605
|
+
const profile = engineProfiles.find(item => item.provider === model.provider && item.model === model.model)
|
|
606
|
+
if (profile) { model.label = `${profile.name} · 仅文字 · ${model.model}`; model.fundingModes = ['subscription'] }
|
|
607
|
+
}
|
|
608
|
+
const localMachineId = machineId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 100) || 'local-host'
|
|
609
|
+
// A CLI identity is only offered once the host verified it against the
|
|
610
|
+
// installed official tool; an unverified route keeps no cliModels entry.
|
|
611
|
+
for (const model of models) {
|
|
612
|
+
for (const mapping of cliConfig?.models ?? []) {
|
|
613
|
+
if (mapping.provider === model.provider && mapping.model === model.model) model.cliModels.push({ toolId: 'codex', model: mapping.cliModel })
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const remoteMachines=new Map()
|
|
617
|
+
for(const profile of cliConfig?.remote??[]){
|
|
618
|
+
if(!remoteMachines.has(profile.machineId))remoteMachines.set(profile.machineId,{machineId:profile.machineId,label:profile.machineId,location:'remote',status:'configured',roots:[]})
|
|
619
|
+
remoteMachines.get(profile.machineId).roots.push({rootId:profile.rootId,label:profile.rootId,modes:['read-only'],capabilities:[]})
|
|
620
|
+
}
|
|
621
|
+
for(const machine of machines?.readModel().items??[]){
|
|
622
|
+
if(machine.status==='revoked')continue
|
|
623
|
+
const roots=machine.roots.map(rootId=>({rootId,label:rootId,modes:['read-only','workspace-write'],capabilities:[...machine.tools]}))
|
|
624
|
+
remoteMachines.set(machine.id,{machineId:machine.id,label:machine.name,location:'remote',status:machine.status,roots})
|
|
625
|
+
}
|
|
626
|
+
let hostProfiles = []
|
|
627
|
+
try {
|
|
628
|
+
const value = typeof config.modelProfilesJson === 'function' ? config.modelProfilesJson() : config.modelProfilesJson
|
|
629
|
+
hostProfiles = parseModelProfilesJson(value ?? '[]')
|
|
630
|
+
} catch { /* An invalid price catalog contributes no suggested prices. */ }
|
|
631
|
+
for (const model of models) {
|
|
632
|
+
const quote = hostProfiles.find(item => item.provider === model.provider && item.model === model.model)?.pricing
|
|
633
|
+
if (quote) {
|
|
634
|
+
const { currency, ...pricing } = quote
|
|
635
|
+
model.pricing = pricing
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { models, tools: cliConfig ? [{ toolId: 'codex', modes: ['read-only'], grantTools: [...CLI_GRANT_TOOLS] }] : [],
|
|
639
|
+
machines: [{ machineId: localMachineId, label: '本机', location: 'local', status: 'ready',
|
|
640
|
+
roots: [{ rootId: 'default', label: '默认工作区', modes: ['read-only', 'workspace-write'], capabilities: [] }] },...remoteMachines.values()],
|
|
641
|
+
capabilities: [
|
|
642
|
+
{ id: 'model-inference', label: '模型推理', tools: ['model/infer'] },
|
|
643
|
+
{ id: 'mr-routing', label: '模型路由', tools: PRODUCT_TOOLS.filter(name=>name!=='model/infer') },
|
|
644
|
+
{ id: 'data-analysis', label: '数据分析与图表', tools: ['tool/data'] },
|
|
645
|
+
{ id: 'team-work', label: '团队协作与委派', tools: ['tool/team', 'tool/coordinate_bots', 'tool/assignment_status', 'tool/list_bots', 'tool/list_threads'] },
|
|
646
|
+
{ ...TASK_CAPABILITY },
|
|
647
|
+
...[...new Set((machines?.readModel().items ?? []).filter(machine => machine.status !== 'revoked').flatMap(machine => machine.tools))]
|
|
648
|
+
.map(tool => ({ id: tool, label: tool, tools: [`remote/${tool}`] })),
|
|
649
|
+
],
|
|
650
|
+
taskBackends: taskBackends ? taskBackends.catalog() : [],
|
|
651
|
+
fundingScopes: fundingAccounts ? fundingAccounts.catalog().filter(scope => scope.enabled).map(scope => ({
|
|
652
|
+
id: scope.id, label: scope.provider, modes: ['api','subscription'],
|
|
653
|
+
models: models.filter(model => model.provider === scope.provider).map(({ provider, model }) => ({ provider, model })),
|
|
654
|
+
})) : [] }
|
|
655
|
+
}
|
|
656
|
+
// N06b: the Bot binding is the funding source of truth for its own model
|
|
657
|
+
// calls. An unconfigured, mismatched or incomplete binding yields null and
|
|
658
|
+
// the accounting layer refuses the call; there is no silent fallback route.
|
|
659
|
+
const bindingRoute = ({ botId, provider, model }) => {
|
|
660
|
+
if (!botSettings) return null
|
|
661
|
+
const value = botSettings.get(botId, { publishChange: false })
|
|
662
|
+
const settings = value.settings
|
|
663
|
+
const route = provider === undefined && model === undefined ? settings.model : { provider, model }
|
|
664
|
+
if (!route?.provider || !route?.model) return null
|
|
665
|
+
const multi = Boolean(settings.mainRoutes && product?.lifecycle.committedMainBotIds().includes(botId))
|
|
666
|
+
let selected = settings.funding
|
|
667
|
+
if (multi) {
|
|
668
|
+
if (!settings.enabled || value.retired || value.errorCode === 'BOT_SETTINGS_SUSPENDED') return null
|
|
669
|
+
try { selected = checkMainRouteFunding(route, fundingForMainRoute(settings.mainRoutes, route), value.catalog) }
|
|
670
|
+
catch { return null }
|
|
671
|
+
} else if (value.status !== 'available' || settings.model?.provider !== route.provider || settings.model?.model !== route.model) {
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
const account = selected.scopeId && fundingAccounts ? fundingAccounts.get(selected.scopeId) : null
|
|
675
|
+
if (multi && (!account?.enabled || account.provider !== route.provider)) return null
|
|
676
|
+
return { route, funding: selected, account, multi, revision: value.revision }
|
|
677
|
+
}
|
|
678
|
+
const bindingFunding = query => {
|
|
679
|
+
const binding = bindingRoute(query)
|
|
680
|
+
if (!binding) return null
|
|
681
|
+
const { route, funding: selected, account, multi, revision } = binding
|
|
682
|
+
const profile = multi ? { fundingRevision: `${revision}:${account.revision}` } : {}
|
|
683
|
+
if (selected.mode === 'subscription') {
|
|
684
|
+
return { funding: 'subscription', route: { ...route }, scopeId: selected.scopeId,
|
|
685
|
+
subscriptionMaxCalls: selected.subscription?.dailyCalls ?? null,
|
|
686
|
+
subscriptionMonthlyCalls: selected.subscription?.monthlyCalls ?? null, ...profile }
|
|
687
|
+
}
|
|
688
|
+
if (!selected.pricing || !selected.request) return null
|
|
689
|
+
return { funding: 'api', route: { ...route }, scopeId: selected.scopeId, pricing: selected.pricing,
|
|
690
|
+
maxInputTokens: selected.request.maxInputTokens, maxOutputTokens: selected.request.maxOutputTokens,
|
|
691
|
+
botDailyLimitUsd: selected.dailyBudgetUsd, botMonthlyLimitUsd: selected.monthlyBudgetUsd, ...profile,
|
|
692
|
+
...(account ? { accountDailyLimitUsd: account.dailyLimitUsd, accountMonthlyLimitUsd: account.monthlyLimitUsd } : {}) }
|
|
693
|
+
}
|
|
694
|
+
const boot = (async () => {
|
|
695
|
+
try{
|
|
696
|
+
const nativeHostRequests = new WeakMap()
|
|
697
|
+
const engineContext = (profile, route, expected = null) => {
|
|
698
|
+
const refuse = code => { throw Object.assign(new Error(code), { code }) }
|
|
699
|
+
const active = policy.activeIdentity()
|
|
700
|
+
if (active?.owner) refuse('CONVERSATION_ENGINE_BOT_REQUIRED')
|
|
701
|
+
const independent = expected ? expected.independent === true : active?.independent === true
|
|
702
|
+
const session = independent ? null : ctx.get('sessions')?.get?.(route.sessionId)
|
|
703
|
+
if (!independent && (!session || session.id !== route.sessionId || !product?.rt.isBotSession(route.sessionId))) refuse('CONVERSATION_ENGINE_HOST_SESSION_REQUIRED')
|
|
704
|
+
const frame = independent ? expected ?? active : policy.modelIdentity(route)
|
|
705
|
+
if (!frame?.botId) refuse('CONVERSATION_ENGINE_BOT_REQUIRED')
|
|
706
|
+
const bot = product?.lifecycle.committedBot(frame.botId)
|
|
707
|
+
if (!bot || profile.provider !== route.provider || profile.model !== route.model) refuse('CONVERSATION_ENGINE_BINDING_CHANGED')
|
|
708
|
+
const binding = bindingRoute({ botId: bot.id, provider: route.provider, model: route.model })
|
|
709
|
+
if (!binding || ((!binding.multi || independent) && bot.model !== `${route.provider}/${route.model}`)) refuse('CONVERSATION_ENGINE_BINDING_CHANGED')
|
|
710
|
+
if (binding.funding.mode !== 'subscription') refuse('CONVERSATION_ENGINE_SUBSCRIPTION_REQUIRED')
|
|
711
|
+
const subject = directory.forBot(bot.id)
|
|
712
|
+
if (subject !== frame.subject) refuse('SUBJECT_MISMATCH')
|
|
713
|
+
product.serving.assertMode(bot.id, independent ? 'independent' : 'integration')
|
|
714
|
+
authority.assertTool(subject, 'model/infer'); authority.assertModel(subject, route.provider, route.model)
|
|
715
|
+
return { bot, subject, independent, session, multi: binding.multi,
|
|
716
|
+
fundingRevision: binding.multi ? `${binding.revision}:${binding.account.revision}` : null }
|
|
717
|
+
}
|
|
718
|
+
const projectNativeRequest = (request, current) => {
|
|
719
|
+
if (current.independent) return request
|
|
720
|
+
const projected = projectHostLoopTextRequest(request, {
|
|
721
|
+
session: current.session, isLoopRequest: isAgentLoopRequest, allowRouteContext: current.multi,
|
|
722
|
+
})
|
|
723
|
+
const bootstrap = current.multi ? hostLoopRouteContext(projected, {
|
|
724
|
+
session: current.session, redact: product.rt.redactSecrets,
|
|
725
|
+
}) : null
|
|
726
|
+
return { request: projected, bootstrap }
|
|
727
|
+
}
|
|
728
|
+
const engineSecret = async secretRef => {
|
|
729
|
+
if (!product) throw Object.assign(new Error('CONVERSATION_ENGINE_CREDENTIAL_UNAVAILABLE'), { code: 'CONVERSATION_ENGINE_CREDENTIAL_UNAVAILABLE' })
|
|
730
|
+
await product.rt.loadSecrets()
|
|
731
|
+
const name = secretRef.slice('ds-secret/'.length)
|
|
732
|
+
const view = product.rt.secretsView().find(secret => secret.name === name)
|
|
733
|
+
if (!view || !Array.isArray(view.scope) || view.scope.length) throw Object.assign(new Error('CONVERSATION_ENGINE_CREDENTIAL_MUST_BE_HOST_ONLY'), { code: 'CONVERSATION_ENGINE_CREDENTIAL_MUST_BE_HOST_ONLY' })
|
|
734
|
+
return product.rt.secretValue(name)
|
|
735
|
+
}
|
|
736
|
+
const checkEngineCredential = async (profile, botId) => {
|
|
737
|
+
let value
|
|
738
|
+
try { value = validateClaudeProfileCredential(await engineSecret(profile.credentialRef)) }
|
|
739
|
+
catch (error) { await conversationEngines?.invalidateBot(profile.id, botId, 'credential-unavailable'); throw error }
|
|
740
|
+
if (typeof value !== 'string' || !value) {
|
|
741
|
+
await conversationEngines?.invalidateBot(profile.id, botId, 'credential-unavailable')
|
|
742
|
+
throw Object.assign(new Error('CONVERSATION_ENGINE_CREDENTIAL_UNAVAILABLE'), { code: 'CONVERSATION_ENGINE_CREDENTIAL_UNAVAILABLE' })
|
|
743
|
+
}
|
|
744
|
+
const key = `${profile.id}\0${botId}`, stamp = createHash('sha256').update(value).digest('hex')
|
|
745
|
+
const previous = engineCredentialStamps.get(key)
|
|
746
|
+
engineCredentialStamps.set(key, stamp)
|
|
747
|
+
if (previous && previous !== stamp) {
|
|
748
|
+
await conversationEngines?.invalidateBot(profile.id, botId, 'credential-changed')
|
|
749
|
+
throw Object.assign(new Error('CONVERSATION_ENGINE_CREDENTIAL_CHANGED'), { code: 'CONVERSATION_ENGINE_CREDENTIAL_CHANGED' })
|
|
750
|
+
}
|
|
751
|
+
return value
|
|
752
|
+
}
|
|
753
|
+
conversationEngines = await mountConversationEngines({ llm: ctx.llm, profiles: engineProfiles,
|
|
754
|
+
runtimeRoot: join(resolveStateHome(), 'model-router', 'conversation-engines'),
|
|
755
|
+
onActivity: (_profile, event) => nativeActivity?.observe(event),
|
|
756
|
+
// Discovery/preparation may run before a Bot frame exists. The route
|
|
757
|
+
// class is subscription-only; identityFor rechecks the Bot's actual
|
|
758
|
+
// committed funding at dispatch, and the one accounting admits it.
|
|
759
|
+
fundingFor: async (profile, route) => {
|
|
760
|
+
if (policy.activeIdentity()?.independent) { const { bot } = engineContext(profile, route); await checkEngineCredential(profile, bot.id) }
|
|
761
|
+
return 'subscription'
|
|
762
|
+
},
|
|
763
|
+
identityFor: async (profile, route) => {
|
|
764
|
+
const { bot, subject, independent, session, fundingRevision } = engineContext(profile, route)
|
|
765
|
+
await checkEngineCredential(profile, bot.id)
|
|
766
|
+
const savedPersona = independentSystemMessage(bot).content.map(part => part.text).join('')
|
|
767
|
+
const persona = independent ? savedPersona : hostSessionPersona(session, savedPersona)
|
|
768
|
+
const conversationId = independent ? policy.activeIdentity()?.conversationId ?? ctx.get?.('botRuntimeAdapter')?.contextOf?.(bot.id)?.contextId ?? null
|
|
769
|
+
: `host-${createHash('sha256').update(session.id).digest('hex')}`
|
|
770
|
+
return { botId: bot.id, subject, authority, fundingRevision, independent, ...(conversationId ? { conversationId } : {}),
|
|
771
|
+
...(session ? { hostSessionId: session.id } : {}),
|
|
772
|
+
// The native process lease has Host-owned local correlation. Individual
|
|
773
|
+
// MCP calls obtain the paired target's own scoped capability lease.
|
|
774
|
+
binding: { taskId: `chat-${createHash('sha256').update(bot.id).digest('hex').slice(0, 24)}`,
|
|
775
|
+
attemptId: engineLeaseId, machineId, workspaceId, location: 'local' },
|
|
776
|
+
persona: product.rt.redactSecrets?.(persona) ?? persona }
|
|
777
|
+
},
|
|
778
|
+
toolsFor: async (profile, identity) => {
|
|
779
|
+
const route = { provider: profile.provider, model: profile.model, sessionId: identity.hostSessionId }
|
|
780
|
+
const validate = async () => {
|
|
781
|
+
const current = engineContext(profile, route, identity)
|
|
782
|
+
if (current.subject !== identity.subject || current.bot.id !== identity.botId) throw Object.assign(new Error('SUBJECT_MISMATCH'), { code: 'SUBJECT_MISMATCH' })
|
|
783
|
+
if (current.fundingRevision !== identity.fundingRevision) throw Object.assign(new Error('MODEL_FUNDING_CHANGED'), { code: 'MODEL_FUNDING_CHANGED' })
|
|
784
|
+
await checkEngineCredential(profile, current.bot.id)
|
|
785
|
+
if (engineContext(profile, route, identity).fundingRevision !== identity.fundingRevision) throw Object.assign(new Error('MODEL_FUNDING_CHANGED'), { code: 'MODEL_FUNDING_CHANGED' })
|
|
786
|
+
}
|
|
787
|
+
await validate()
|
|
788
|
+
// Discovery must remain available to a consult turn. The execution
|
|
789
|
+
// boundary enforces its read-only task after ordinary Host admission.
|
|
790
|
+
const actValidate = async () => {
|
|
791
|
+
await validate()
|
|
792
|
+
teamCoordination?.assertCanAct(identity.botId, { sessionId: identity.hostSessionId, conversationId: identity.conversationId })
|
|
793
|
+
}
|
|
794
|
+
const view = ctx.get('botIndependentToolHost').forBot(identity.botId, { nativeRoute: route })
|
|
795
|
+
const ordinary = createNativeConversationTools({ view, validate: actValidate, redact: product.rt.redactSecrets })
|
|
796
|
+
const remote = createNativeConversationTools({
|
|
797
|
+
view: nativeRemoteActions.forConversation(identity.botId, identity.conversationId), validate: actValidate,
|
|
798
|
+
entryFor: nativeRemoteToolEntry, render: renderNativeRemoteResult, redact: product.rt.redactSecrets,
|
|
799
|
+
})
|
|
800
|
+
const data = createNativeDataTools({ access: dataAccess, subject: identity.subject,
|
|
801
|
+
botId: identity.botId, validate: actValidate, redact: product.rt.redactSecrets })
|
|
802
|
+
const team = teamCoordination?.canUse(identity.botId) ? createNativeTeamTools({ service: teamCoordination,
|
|
803
|
+
callerBotId: identity.botId, sessionId: identity.hostSessionId, conversationId: identity.conversationId,
|
|
804
|
+
validate: actValidate, redact: product.rt.redactSecrets }) : { tools: [], handlers: [] }
|
|
805
|
+
return { tools: [...ordinary.tools, ...remote.tools, ...data.tools, ...team.tools], handlers: [...ordinary.handlers, ...remote.handlers, ...data.handlers, ...team.handlers] }
|
|
806
|
+
},
|
|
807
|
+
requestFor: (profile, request, identity) => {
|
|
808
|
+
const current = engineContext(profile, request)
|
|
809
|
+
if (current.independent) return request
|
|
810
|
+
// The SDK may project tool-update messages away before it invokes
|
|
811
|
+
// the adapter. Use the original loop request admitted by this Host.
|
|
812
|
+
const admitted = nativeHostRequests.get(request.signal)
|
|
813
|
+
nativeHostRequests.delete(request.signal)
|
|
814
|
+
const original = admitted?.request
|
|
815
|
+
if (!original || original.sessionId !== request.sessionId || original.provider !== request.provider || original.model !== request.model) {
|
|
816
|
+
throw Object.assign(new Error('CLAUDE_HOST_LOOP_REQUEST_REQUIRED'), { code: 'CLAUDE_HOST_LOOP_REQUEST_REQUIRED' })
|
|
817
|
+
}
|
|
818
|
+
if (admitted.revision !== current.fundingRevision || identity.fundingRevision !== current.fundingRevision) {
|
|
819
|
+
throw Object.assign(new Error('MODEL_FUNDING_CHANGED'), { code: 'MODEL_FUNDING_CHANGED' })
|
|
820
|
+
}
|
|
821
|
+
return projectNativeRequest(original, current)
|
|
822
|
+
},
|
|
823
|
+
resolveSecret: async (_secretRef, profile, identity) => {
|
|
824
|
+
const verify = () => {
|
|
825
|
+
const current = engineContext(profile, { provider: profile.provider, model: profile.model, sessionId: identity?.hostSessionId })
|
|
826
|
+
if (current.subject !== identity?.subject) throw Object.assign(new Error('SUBJECT_MISMATCH'), { code: 'SUBJECT_MISMATCH' })
|
|
827
|
+
if (current.fundingRevision !== identity.fundingRevision) throw Object.assign(new Error('MODEL_FUNDING_CHANGED'), { code: 'MODEL_FUNDING_CHANGED' })
|
|
828
|
+
return current.bot.id
|
|
829
|
+
}
|
|
830
|
+
const value = await checkEngineCredential(profile, verify())
|
|
831
|
+
verify()
|
|
832
|
+
return value
|
|
833
|
+
} })
|
|
834
|
+
ctx.provide?.('botConversationEngines', Object.freeze({ catalog: conversationEngines.catalog }))
|
|
835
|
+
ctx.provide?.('botIndependentConversationRetirement', async (botId, { signal, reason = 'new-conversation' } = {}) => {
|
|
836
|
+
signal?.throwIfAborted()
|
|
837
|
+
if (!product?.lifecycle.committedBot(botId)) throw Object.assign(new Error('BOT_NOT_COMMITTED'), { code: 'BOT_NOT_COMMITTED' })
|
|
838
|
+
product.serving.assertMode(botId, 'independent')
|
|
839
|
+
teamCoordination?.stop(botId)
|
|
840
|
+
return conversationEngines.retireBot(botId, reason)
|
|
841
|
+
})
|
|
842
|
+
await Promise.race([refresh(), new Promise(resolve => { const pending = setTimeout(resolve, INITIAL_ROUTE_TIMEOUT_MS); pending.unref() })])
|
|
843
|
+
if(deferredRouterState){
|
|
844
|
+
sharedAdmission=await acquireMigrationShared(resolveStateHome())
|
|
845
|
+
assertMigrationStartupAllowed({home:resolveStateHome()})
|
|
846
|
+
funding=await openProductFunding({Database,file:storageFile,legacyFile,policy,authority,pricingFor:bindingFunding,now})
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// The independent Bot runtime may call a remote tool, but only through this: the Bot's own
|
|
850
|
+
// subject, its first live machine binding, and a fresh task/attempt per call. Nothing here
|
|
851
|
+
// accepts a machine, root, endpoint or operation id from the caller's arguments.
|
|
852
|
+
ctx.provide?.('botRemoteToolInvoker', Object.freeze({
|
|
853
|
+
async invoke(botId, tool, args = {}, { signal } = {}) {
|
|
854
|
+
signal?.throwIfAborted()
|
|
855
|
+
const subject = directory.forBot(botId)
|
|
856
|
+
if (!subject) throw new Error('UNKNOWN_BOT')
|
|
857
|
+
const model = machines.readModel()
|
|
858
|
+
const candidates = (model?.items ?? []).filter(item => item.status !== 'revoked'
|
|
859
|
+
&& Array.isArray(item.bindings) && item.bindings.some(binding => binding.botId === botId && !binding.revoked))
|
|
860
|
+
// A caller may name one machine; it must be one of this Bot's own live bindings.
|
|
861
|
+
const wanted = typeof args?.machineId === 'string' ? args.machineId : null
|
|
862
|
+
const machine = wanted ? candidates.find(item => item.id === wanted) : candidates[0]
|
|
863
|
+
if (!machine) throw new Error('MACHINE_NOT_BOUND')
|
|
864
|
+
const binding = machine.bindings.find(item => item.botId === botId && !item.revoked)
|
|
865
|
+
const rootId = (binding.roots ?? [])[0]
|
|
866
|
+
if (typeof rootId !== 'string' || !rootId) throw new Error('MACHINE_ROOT_NOT_BOUND')
|
|
867
|
+
if (Array.isArray(binding.tools) && !binding.tools.includes(tool)) throw new Error('MACHINE_BOT_DENIED')
|
|
868
|
+
const calls = { ...args }
|
|
869
|
+
delete calls.machineId
|
|
870
|
+
const stamp = String(Date.now()) + Math.random().toString(16).slice(2, 8)
|
|
871
|
+
return machines.call(subject, {
|
|
872
|
+
machineId: machine.id, rootId, taskId: 'bot-' + stamp, attemptId: 'bot-a-' + stamp, tool, args: calls,
|
|
873
|
+
}, { signal })
|
|
874
|
+
},
|
|
875
|
+
}))
|
|
876
|
+
routingSettings = openBotRoutingSettings({ Database, file: storageFile, initial: () => config.botRouting, now })
|
|
877
|
+
uiNamespace = openUiHostNamespace({ Database, file: storageFile })
|
|
878
|
+
nativeActivity = createNativeActivityService({ Database, file: storageFile, namespace: uiNamespace.read().hostId,
|
|
879
|
+
committedBot: botId => product?.lifecycle.committedBot(botId) ?? null,
|
|
880
|
+
nativeRouteForBot: botId => engineProfiles.some(profile => Boolean(bindingRoute({ botId, provider: profile.provider, model: profile.model }))),
|
|
881
|
+
redactToolName: value => product?.rt.redactSecrets?.(value) ?? value, now })
|
|
882
|
+
ctx.provide('botNativeActivity', nativeActivity)
|
|
883
|
+
const webServer=(()=>{try{return ctx.get?.('webServer')}catch{return null}})()
|
|
884
|
+
if(webServer?.registerUpgrade&&ctx.connection?.admitConnection)desktopProxy=createDesktopProxy({webServer,connection:ctx.connection,now})
|
|
885
|
+
desktopHttp=createDesktopHttpCarrier({now})
|
|
886
|
+
machines = openRemoteMachines({ Database, file:storageFile, authority, desktopProxy, desktopHttp, cliProfiles:cliConfig?.remote??[],
|
|
887
|
+
botExists:id=>Boolean(product?.lifecycle.committedBot(id)), mainBotIds: () => product?.lifecycle.committedMainBotIds() ?? [], now,
|
|
888
|
+
resolveSecret:async secretRef=>{
|
|
889
|
+
if(!product)throw new Error('MACHINE_SECRET_UNAVAILABLE')
|
|
890
|
+
await product.rt.loadSecrets()
|
|
891
|
+
const name=secretRef.slice('ds-secret/'.length)
|
|
892
|
+
const view=product.rt.secretsView().find(secret=>secret.name===name)
|
|
893
|
+
// A machine control-plane key must never be offered to Bot shells.
|
|
894
|
+
if(!view||!Array.isArray(view.scope)||view.scope.length)throw new Error('MACHINE_SECRET_MUST_BE_HOST_ONLY')
|
|
895
|
+
return product.rt.secretValue(name)
|
|
896
|
+
},
|
|
897
|
+
onChanged:async()=>{if(product)await product.lifecycle.refreshGrants()},
|
|
898
|
+
})
|
|
899
|
+
nativeRemoteActions = openNativeRemoteActions({ Database, file: storageFile, directory, authority, machines: () => machines, now })
|
|
900
|
+
ctx.provide('botNativeRemoteActions', nativeRemoteActions)
|
|
901
|
+
// N06b: the components own grants and admission. A published compiled grant
|
|
902
|
+
// takes effect immediately and a suspension revokes it; an account change
|
|
903
|
+
// re-evaluates every binding because the scope catalog follows it.
|
|
904
|
+
if (deferredRouterState) {
|
|
905
|
+
fundingAccounts = openFundingAccounts({ Database, file: storageFile,
|
|
906
|
+
providers: () => [...new Set([...routes].map(key => key.slice(0, key.indexOf('/'))))],
|
|
907
|
+
onCommitted: () => { try { botSettings?.refresh() } catch { /* the interval retries */ } },
|
|
908
|
+
onSuspended: () => { try { botSettings?.refresh() } catch { /* the interval retries */ } }, now })
|
|
909
|
+
botSettings = openBotSettings({ Database, file: storageFile,
|
|
910
|
+
catalogFor: settingsCatalog,
|
|
911
|
+
botExists: id => committedBots.has(id),
|
|
912
|
+
isMainBot: id => product?.lifecycle.committedMainBotIds().includes(id) === true,
|
|
913
|
+
onCommitted: value => {
|
|
914
|
+
if (!product?.lifecycle.committedMainBotIds().includes(value.botId)) {
|
|
915
|
+
try { directory.replaceGrant(value.botId, value.grant) } catch { /* the lifecycle recompiles */ }
|
|
916
|
+
}
|
|
917
|
+
grantRefresh.request()
|
|
918
|
+
},
|
|
919
|
+
onSuspended: value => {
|
|
920
|
+
if (!product?.lifecycle.committedMainBotIds().includes(value.botId)) {
|
|
921
|
+
try { directory.replaceGrant(value.botId, { tools: [], modelRoutes: [] }) } catch { /* the lifecycle recompiles */ }
|
|
922
|
+
}
|
|
923
|
+
grantRefresh.request()
|
|
924
|
+
}, now })
|
|
925
|
+
const modelBounds = createModelBoundResolver({ llm: () => ctx.llm })
|
|
926
|
+
modelAccounting = createModelAccounting({ store: funding.store, authority,
|
|
927
|
+
pricingFor: bindingFunding,
|
|
928
|
+
fundingRevisionFor: query => bindingFunding(query)?.fundingRevision ?? null,
|
|
929
|
+
preparedFrameFor: async request => {
|
|
930
|
+
const captured = modelFrames.read(request.signal)
|
|
931
|
+
if (captured) return captured
|
|
932
|
+
const verdict = sessionLoopFrame(request)
|
|
933
|
+
if (verdict.kind === 'frame') {
|
|
934
|
+
const stamp = loopFunding.get(request.signal)
|
|
935
|
+
return stamp?.sessionId === request.sessionId && stamp.provider === request.provider && stamp.model === request.model
|
|
936
|
+
? { ...verdict.frame, fundingRevision: stamp.revision }
|
|
937
|
+
: verdict.frame
|
|
938
|
+
}
|
|
939
|
+
if (verdict.kind === 'aux') return null
|
|
940
|
+
if (verdict.kind === 'unsupportedHost') {
|
|
941
|
+
const identity = policy.modelIdentity(request)
|
|
942
|
+
const funding = await bindingFunding({ botId: identity.botId, provider: request.provider, model: request.model })
|
|
943
|
+
if (funding?.funding === 'subscription') return null
|
|
944
|
+
}
|
|
945
|
+
throw Object.assign(new Error('本次调用无法确认已绑定的模型容量,请核对宿主会话记录。'), {
|
|
946
|
+
code: verdict.kind === 'missing' ? 'MODEL_LOOP_FRAME_MISSING' : verdict.kind === 'unsupportedHost' ? 'MODEL_LOOP_FRAME_UNSUPPORTED' : 'MODEL_LOOP_FRAME_INVALID',
|
|
947
|
+
reason: verdict.reason, outcome: 'refused' })
|
|
948
|
+
},
|
|
949
|
+
modelBoundsFor: async ({ options, signal }) => {
|
|
950
|
+
const bounds = await modelBounds.bounds({ provider: options.provider, model: options.model, signal })
|
|
951
|
+
return { inputBound: bounds.contextWindow, outputDefault: bounds.defaultMaxTokens }
|
|
952
|
+
},
|
|
953
|
+
machineId, workspaceId, now,
|
|
954
|
+
// The scoped admission (Bot/account/project budgets) is authoritative;
|
|
955
|
+
// the ledger limit stays open so no hidden global cap shadows it.
|
|
956
|
+
limits: { dailyLimitUsd: 0, monthlyLimitUsd: 0 } })
|
|
957
|
+
// N09: one managed Task runner over the same SQLite authority. The
|
|
958
|
+
// journal, funding store and component tables share orchestration.sqlite;
|
|
959
|
+
// backends stay stable per kind while each Bot's binding supplies the
|
|
960
|
+
// execution target, model and money at run time.
|
|
961
|
+
if (typeof mr?.executeConfiguredAssignment !== 'function' || typeof mr?.rerunRecordedStep !== 'function'
|
|
962
|
+
|| typeof mr?.routerStateStore !== 'function') throw new TypeError('BOT_PRODUCT_MR_SEAM_REQUIRED')
|
|
963
|
+
const managedRoot = join(dirname(storageFile), 'managed')
|
|
964
|
+
const managedWorkspace = join(managedRoot, 'read-workspace')
|
|
965
|
+
await mkdir(managedWorkspace, { recursive: true })
|
|
966
|
+
if (cliConfig?.entry&&cliConfig.sessionFactory) {
|
|
967
|
+
const factory = await import(cliConfig.sessionFactory).then(module => module.createCliSession ?? module.default, () => null)
|
|
968
|
+
if (typeof factory !== 'function') throw new TypeError('TASK_CLI_SESSION_FACTORY_REQUIRED')
|
|
969
|
+
cliSessionFactory = factory
|
|
970
|
+
}
|
|
971
|
+
journal = funding.store.openTaskJournal({leaseClock:'db'})
|
|
972
|
+
notifications = createBotNotifications({ Database, file: join(dirname(storageFile), 'notifications.sqlite'), journal,
|
|
973
|
+
botIds: () => product?.lifecycle.committedBots().map(bot => bot.id) ?? [],
|
|
974
|
+
botNames: () => Object.fromEntries((product?.lifecycle.committedBots() ?? []).map(bot => [bot.id, bot.name ?? bot.id])) })
|
|
975
|
+
const modelBackends = createTaskBackends({ ctx, mr, authority, directory, store: funding.store,
|
|
976
|
+
machineId, workspaceId, workspace: managedWorkspace, runtimeRoot: join(managedRoot, 'cli-runtime'),
|
|
977
|
+
bindingFor: botId => botSettings.get(botId, { publishChange: false }),
|
|
978
|
+
cli: cliConfig?.entry ? { entry: cliConfig.entry, sessionFactory: cliSessionFactory ?? (await import('../../cli-runtime/src/codex-runtime.mjs')).createCodexSession } : null })
|
|
979
|
+
const remoteWrite=createRemoteWriteBackend({machines:()=>machines,directory})
|
|
980
|
+
const remoteCommand=createRemoteCommandBackend({machines:()=>machines,directory})
|
|
981
|
+
turnBackends = createBotTurnBackends({ ctx, runtime: () => product?.rt, directory, authority, Database, storeFile: storageFile,
|
|
982
|
+
bindingFor: botId => botSettings.get(botId, { publishChange: false }), machines: () => machines,
|
|
983
|
+
readPlan: report => taskService.getPlan(directory.forBot(report.originBotId), { planId: report.planId }), now })
|
|
984
|
+
if(cliConfig?.remote.length)remoteCliBackend=createRemoteCliReadBackend({machines:()=>machines,directory,store:funding.store,authority,profiles:cliConfig.remote,bindingFor:botId=>botSettings.get(botId,{publishChange:false})})
|
|
985
|
+
const remoteBackends=[remoteWrite,remoteCommand,...(remoteCliBackend?[remoteCliBackend]:[])]
|
|
986
|
+
taskBackends=Object.freeze({backends:Object.freeze([...modelBackends.backends,...remoteBackends]),catalog:()=>[...modelBackends.catalog(),...remoteBackends.map(backend=>({id:backend.id,version:backend.version,effect:backend.effect,location:'remote',capability:backend.capability,label:backend.id==='remote-codex-read'?'已授权远程官方 CLI 只读任务':backend.id==='remote-write'?'已授权远程文件写入任务':'已授权容器命令任务(仅别名argv)'}))]})
|
|
987
|
+
taskService = createTaskService({ journal, authority, directory, backends: [...turnBackends.backends, ...taskBackends.backends],
|
|
988
|
+
within: (subject, signal, work) => policy.executionContext(subject, signal, work),
|
|
989
|
+
planTargetsFor: (subject,plan,caller) => {
|
|
990
|
+
const id=authority.describe(subject).botId
|
|
991
|
+
const bot=product?.lifecycle.committedBot(id)
|
|
992
|
+
const privateSession=product?.rt.chatOf(id)??bot?.sessionId??bot?.id
|
|
993
|
+
const privateCall=caller.owner===true||bot&&caller.sessionId===privateSession
|
|
994
|
+
if(caller.sessionId!==undefined&&plan.origin.kind==='private'&&!privateCall)throw Object.assign(new Error('TASK_PLAN_ORIGIN_MISMATCH'),{code:'TASK_PLAN_ORIGIN_MISMATCH'})
|
|
995
|
+
if(caller.owner===true&&plan.origin.kind==='private'&&plan.origin.sessionId!==null&&plan.origin.sessionId!==privateSession)throw Object.assign(new Error('TASK_PLAN_ORIGIN_MISMATCH'),{code:'TASK_PLAN_ORIGIN_MISMATCH'})
|
|
996
|
+
return privateCall&&['private','handoff'].includes(plan.origin.kind)&&product?.lifecycle.committedMainBotIds().includes(id)?product.lifecycle.committedBots().map(bot=>bot.id):[id]
|
|
997
|
+
},
|
|
998
|
+
runnerTtlMs: TASK_RUNNER_TTL_MS, taskTtlMs: TASK_TTL_MS,
|
|
999
|
+
// Lease expiry reads the ledger's clock, not this process's: a Host
|
|
1000
|
+
// whose wall clock drifted must not be able to declare its own live
|
|
1001
|
+
// lease expired and let a second runner take the same tasks.
|
|
1002
|
+
leaseClock: 'db',
|
|
1003
|
+
// The deployment refuses a writer that cannot prove a stop, so a new
|
|
1004
|
+
// backend cannot be added without also teaching it how to say so.
|
|
1005
|
+
requireStopProof: true,
|
|
1006
|
+
// The committed binding's maxConcurrentTasks is the per-Bot limit; an
|
|
1007
|
+
// unconfigured Bot keeps the conservative single-task default.
|
|
1008
|
+
concurrencyFor: botId => {
|
|
1009
|
+
try {
|
|
1010
|
+
const value = botSettings?.get(botId, { publishChange: false })
|
|
1011
|
+
return value?.status === 'available' ? value.settings.maxConcurrentTasks ?? 1 : 1
|
|
1012
|
+
} catch { return 1 }
|
|
1013
|
+
} })
|
|
1014
|
+
// A previous Host that died without releasing its runner lease blocks the
|
|
1015
|
+
// claim until the lease expires; waiting is safer than dropping tasks.
|
|
1016
|
+
const claimDeadline = now() + TASK_RUNNER_CLAIM_MS
|
|
1017
|
+
for (;;) {
|
|
1018
|
+
try { taskService.start(); break } catch (error) {
|
|
1019
|
+
if (error?.code !== 'TASK_RUNNER_BUSY' || now() >= claimDeadline) throw error
|
|
1020
|
+
await new Promise(resolve => { const pending = setTimeout(resolve, 250); pending.unref() })
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// A runner that stops (a lease lost to a clock jump, a journal write that
|
|
1024
|
+
// never settled) must come back on its own, or the Host keeps accepting
|
|
1025
|
+
// work nobody will claim. The watchdog reports what it did instead of
|
|
1026
|
+
// hiding a permanent stop behind a quiet 5-second tick.
|
|
1027
|
+
taskRunnerWatchdog = setInterval(() => {
|
|
1028
|
+
try {
|
|
1029
|
+
const state = taskService.state()
|
|
1030
|
+
if (state.closed || state.running) return
|
|
1031
|
+
const outcome = taskService.restart({ reason: state.stopReason ?? 'TASK_RUNNER_WATCHDOG' })
|
|
1032
|
+
if (!outcome.accepted) ctx.logger?.('task-runner-restart-deferred', { code: outcome.code, reason: state.stopReason })
|
|
1033
|
+
} catch (error) { ctx.logger?.('task-runner-watchdog-failed', { code: error?.code ?? 'TASK_RUNNER_WATCHDOG_FAILED' }) }
|
|
1034
|
+
}, TASK_RUNNER_WATCHDOG_MS)
|
|
1035
|
+
taskRunnerWatchdog.unref?.()
|
|
1036
|
+
releaseTaskTools = registerTaskTools(ctx, defineTool, { service: taskService, directory })
|
|
1037
|
+
}
|
|
1038
|
+
const machineActions=Object.fromEntries(['pair','bind','probe','revoke','reconcile'].map(name=>[`machine.${name}`,(payload,context)=>machines[name](payload,context)]))
|
|
1039
|
+
machineActions['machine.pair'] = (payload, context) => pairMainMachine({ machines,
|
|
1040
|
+
mainBotId: () => product?.lifecycle.committedMainBotIds()[0] }, payload, context)
|
|
1041
|
+
// Pinned web panels: the owner's own short list of pages, in its own component tables
|
|
1042
|
+
// beside the other authorities in the same SQLite file. A Bot has no path to it —
|
|
1043
|
+
// nothing here is registered as a model tool, and the probe reads a stored pin.
|
|
1044
|
+
webPanelStore = openWebPanelStore({ Database, file: storageFile })
|
|
1045
|
+
webPanelActions = createWebPanelActions({ store: webPanelStore })
|
|
1046
|
+
securePairing = openSecurePairingHost({ Database, file: storageFile, machines, now,
|
|
1047
|
+
botExists: id => typeof id === 'string' && Boolean(product && Object.hasOwn(product.rt.state?.bots ?? {},id)),
|
|
1048
|
+
saveSecret: (name,value) => product.rt.secretEndpoint('secret-set',{name,value,scope:[],purpose:'远程电脑端到端连接凭据,仅供宿主使用。'}),
|
|
1049
|
+
resolveSecret: async reference => {
|
|
1050
|
+
await product.rt.loadSecrets()
|
|
1051
|
+
const name=reference.slice('ds-secret/'.length),view=product.rt.secretsView().find(item=>item.name===name)
|
|
1052
|
+
if(!view)return undefined
|
|
1053
|
+
if(!Array.isArray(view.scope)||view.scope.length)throw new Error('MACHINE_SECRET_MUST_BE_HOST_ONLY')
|
|
1054
|
+
return product.rt.secretValue(name)
|
|
1055
|
+
},
|
|
1056
|
+
pair: (payload,context) => pairMainMachine({ machines, mainBotId: () => product?.lifecycle.committedMainBotIds()[0] },payload,context),
|
|
1057
|
+
})
|
|
1058
|
+
Object.assign(machineActions, { 'machine.pair.invite': (payload,context) => {
|
|
1059
|
+
if(context?.kind!=='owner')throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1060
|
+
return securePairing.redeem(payload,context)
|
|
1061
|
+
} })
|
|
1062
|
+
machineActions['machine.artifact']=(payload,context)=>machines.download(taskOwner(payload),payload,context)
|
|
1063
|
+
// Desktop control: a person watches one frame and acts on it. Both calls run as that
|
|
1064
|
+
// person's Bot, so the Bot grant and the machine ceiling decide, and the operations land
|
|
1065
|
+
// under that Bot instead of an anonymous console.
|
|
1066
|
+
const desktopActions = createDesktopActions({ machines, directory })
|
|
1067
|
+
Object.assign(machineActions, {
|
|
1068
|
+
'machine.desktop.poll': (payload,context) => {
|
|
1069
|
+
if(context?.kind!=='owner')throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1070
|
+
if(!payload||typeof payload.botId!=='string')throw new Error('INVALID_DESKTOP_REQUEST')
|
|
1071
|
+
const {botId,...request}=payload;taskOwner({botId})
|
|
1072
|
+
return desktopHttp.poll(request,{...context,botId})
|
|
1073
|
+
},
|
|
1074
|
+
'machine.desktop.send': (payload,context) => {
|
|
1075
|
+
if(context?.kind!=='owner')throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1076
|
+
if(!payload||typeof payload.botId!=='string')throw new Error('INVALID_DESKTOP_REQUEST')
|
|
1077
|
+
const {botId,...request}=payload;taskOwner({botId})
|
|
1078
|
+
return desktopHttp.send(request,{...context,botId})
|
|
1079
|
+
},
|
|
1080
|
+
})
|
|
1081
|
+
for (const [action, method] of [['open', 'desktopSession'], ['renew', 'renewDesktopSession'], ['close', 'closeDesktopSession']]) {
|
|
1082
|
+
machineActions[`machine.desktop.${action}`] = (payload, context) => {
|
|
1083
|
+
if (context?.kind !== 'owner') throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1084
|
+
if (!payload || typeof payload.botId !== 'string') throw new Error('INVALID_DESKTOP_REQUEST')
|
|
1085
|
+
const { botId, ...request } = payload
|
|
1086
|
+
return machines[method](taskOwner({ botId }), request, context)
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
machineActions['machine.desktop.view'] = (payload, context) => {
|
|
1090
|
+
if (context?.kind !== 'owner') throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1091
|
+
return desktopActions.view(payload)
|
|
1092
|
+
}
|
|
1093
|
+
machineActions['machine.desktop.act'] = (payload, context) => {
|
|
1094
|
+
if (context?.kind !== 'owner') throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1095
|
+
return desktopActions.act(payload)
|
|
1096
|
+
}
|
|
1097
|
+
machineActions['machine.secret']=async(payload,context)=>{
|
|
1098
|
+
if(context?.kind!=='owner')throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1099
|
+
if(!payload||Object.keys(payload).some(key=>!['name','value'].includes(key))||!/^REMOTE_AGENT_[A-Z0-9_]{1,50}$/.test(payload.name)||typeof payload.value!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(payload.value))throw new Error('INVALID_MACHINE_SECRET')
|
|
1100
|
+
const secretRef=`ds-secret/${payload.name}`
|
|
1101
|
+
for(const machine of machines.readModel().items.filter(item=>item.status!=='revoked'&&item.secretRef===secretRef))await machines.revoke({machineId:machine.id,expectedRevision:machine.revision},context)
|
|
1102
|
+
await product.rt.secretEndpoint('secret-set',{name:payload.name,value:payload.value,scope:[],purpose:'远程电脑控制面凭据,仅供宿主使用;不得授予 Bot shell。'})
|
|
1103
|
+
return {secretRef,stored:true}
|
|
1104
|
+
}
|
|
1105
|
+
const botLibrary = (() => { try { return ctx.get?.('botLibrary') ?? null } catch { return null } })()
|
|
1106
|
+
dataEngine = createDataEngine({ dataDir: join(dirname(storageFile), 'data'),
|
|
1107
|
+
...(botLibrary ? { libraryReader: (botId, relativePath) => botLibrary.read(botId, relativePath),
|
|
1108
|
+
libraryWriter: (botId, relativePath, content) => botLibrary.write(botId, relativePath, content) } : {}) })
|
|
1109
|
+
dataAccess = createProductDataAccess({ engine: dataEngine, authority, directory,
|
|
1110
|
+
lifecycle: { committedBot: botId => product?.lifecycle.committedBot(botId) } })
|
|
1111
|
+
const dataAction = (name, payload) => dataAccess.ownerAction(name, payload)
|
|
1112
|
+
const botRuntimeAdapter = (() => { try { return ctx.get?.('botRuntimeAdapter') ?? null } catch { return null } })()
|
|
1113
|
+
const routineStore = (() => { try { return ctx.get?.('routineStore') ?? null } catch { return null } })()
|
|
1114
|
+
if (routineStore) routineExecution = createRoutineExecutionService({ machines, store: routineStore, botIdForSubject: describeBot })
|
|
1115
|
+
product = createBotProduct(ctx, { authority, directory, policy, grantFor, threadHost: () => botThreads,
|
|
1116
|
+
modelFrames, fundingFor: bindingFunding,
|
|
1117
|
+
independentReadyFor: (botId, route) => Boolean(bindingRoute({ botId, ...route })),
|
|
1118
|
+
prepareLoopFunding: ({ botId, sessionId, signal, config, previousConfig }) => {
|
|
1119
|
+
if (!signal || !product.lifecycle.committedMainBotIds().includes(botId)) return config
|
|
1120
|
+
const { provider, model } = config
|
|
1121
|
+
const quote = bindingFunding({ botId, provider, model })
|
|
1122
|
+
loopFunding.set(signal, { sessionId, provider, model, revision: quote?.fundingRevision ?? null })
|
|
1123
|
+
if (!quote?.fundingRevision) return config
|
|
1124
|
+
const switched = previousConfig && (previousConfig.provider !== config.provider || previousConfig.model !== config.model)
|
|
1125
|
+
// AgentLoop seeds the new proposal from the last request header. Its
|
|
1126
|
+
// previous route's output cap must not follow a deliberate switch.
|
|
1127
|
+
if (switched && previousConfig.maxTokens === config.maxTokens && config.maxTokens !== undefined) {
|
|
1128
|
+
const { maxTokens: _previousCap, ...selected } = config
|
|
1129
|
+
config = selected
|
|
1130
|
+
}
|
|
1131
|
+
if (quote.funding !== 'api') return config
|
|
1132
|
+
if (config.maxTokens !== undefined && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens < 1)) return config
|
|
1133
|
+
const maxTokens = config.maxTokens === undefined ? quote.maxOutputTokens : Math.min(config.maxTokens, quote.maxOutputTokens)
|
|
1134
|
+
return { ...config, maxTokens }
|
|
1135
|
+
},
|
|
1136
|
+
isTextOnlyModel: route => engineProfiles.some(profile => profile.provider === route.provider && profile.model === route.model),
|
|
1137
|
+
beforeModelRequest: async request => {
|
|
1138
|
+
const profile = engineProfiles.find(item => item.provider === request.provider && item.model === request.model)
|
|
1139
|
+
if (!profile) return
|
|
1140
|
+
const current = engineContext(profile, request)
|
|
1141
|
+
projectNativeRequest(request, current)
|
|
1142
|
+
await checkEngineCredential(profile, current.bot.id)
|
|
1143
|
+
if (!current.independent && request.signal) nativeHostRequests.set(request.signal, { request, revision: current.fundingRevision })
|
|
1144
|
+
},
|
|
1145
|
+
accountStream: (streamOptions, next, meta) => {
|
|
1146
|
+
turnBackends?.tagModelRequest(streamOptions)
|
|
1147
|
+
const enriched = meta?.subject ? { ...meta, botId: describeBot(meta.subject) } : meta
|
|
1148
|
+
// N06b: the unified admission owns every Bot model call; the diagnostic
|
|
1149
|
+
// decorator only remains for lightweight contexts without the store.
|
|
1150
|
+
if (!modelAccounting) return accountStream(streamOptions, next, enriched)
|
|
1151
|
+
// A direct MR call inside an admitted execution frame references that
|
|
1152
|
+
// run's invoice instead of opening a second Bot-side one.
|
|
1153
|
+
funding?.funded?.bindModelRequest?.(streamOptions, { agentLoop: meta?.agentLoop === true, ownRequest: meta?.ownRequest === true })
|
|
1154
|
+
return modelAccounting.stream(streamOptions, next, { ...enriched, source: RECEIPT_SOURCE[enriched?.source] ?? enriched?.source })
|
|
1155
|
+
},
|
|
1156
|
+
onBind, runtimeConfig,
|
|
1157
|
+
readModels: { bindings, machines:()=>machines.readModel(),
|
|
1158
|
+
botBindings: async ({ runtime, signal }) => {
|
|
1159
|
+
signal?.throwIfAborted(); await runtime.load()
|
|
1160
|
+
const catalog = settingsCatalog()
|
|
1161
|
+
return { ...botSettings.readModel(Object.values(runtime.state?.bots ?? {})),
|
|
1162
|
+
machines: catalog.machines.map(m => ({ machineId: m.machineId, label: m.label, location: m.location, roots: m.roots.map(r => ({ rootId: r.rootId, label: r.label, modes: r.modes })) })),
|
|
1163
|
+
fundingScopes: catalog.fundingScopes }
|
|
1164
|
+
},
|
|
1165
|
+
fundingAccounts: async () => ({ status: 'available', items: fundingAccounts.catalog() }),
|
|
1166
|
+
// Pinned web panels read the owner's own directory through the same single
|
|
1167
|
+
// authority as every other projection; a section without it stays blocked.
|
|
1168
|
+
webPanels: async () => webPanelActions.provider(),
|
|
1169
|
+
threads: async ({ runtime }) => {
|
|
1170
|
+
if (!botThreads?.ready) return { status: 'blocked', items: [], reason: '当前宿主未提供完整线程会话能力。', nextAction: '更新插件并重新加载宿主后重试。' }
|
|
1171
|
+
const views = Object.keys(runtime.state.bots).map(botId => ({ botId, ...botThreads.actions['threads.readModel']({ botId }) }))
|
|
1172
|
+
return { status: 'available', items: views.flatMap(view => view.items.map(item => ({ ...item, botId: view.botId }))),
|
|
1173
|
+
bots: views.map(({ botId, status, reason, nextAction, activeThreadId }) => ({ botId, status, reason, nextAction, activeThreadId })) }
|
|
1174
|
+
},
|
|
1175
|
+
// N09: the managed Task projection reads the durable journal through the
|
|
1176
|
+
// same single authority; a section without a runner stays blocked.
|
|
1177
|
+
...(taskService ? { tasks: async ({ runtime, signal }) => {
|
|
1178
|
+
signal?.throwIfAborted(); await runtime.load()
|
|
1179
|
+
const loaded = journal.productList(Object.values(runtime.state?.bots ?? {}).map(bot => bot.id), { limit: TASK_READ_LIMIT })
|
|
1180
|
+
const items = loaded.items.map(projectProductTask)
|
|
1181
|
+
const plans=Object.values(runtime.state?.bots??{}).flatMap(bot=>journal.listPlans(bot.id))
|
|
1182
|
+
return { status: 'available', items, total: loaded.total, truncated: loaded.truncated, plans, backends: taskBackends.catalog() }
|
|
1183
|
+
} } : {}),
|
|
1184
|
+
// N07: the usage projection reads the durable source receipts and the
|
|
1185
|
+
// funding totals from the single authority; an unknown amount stays
|
|
1186
|
+
// null instead of being rendered as zero.
|
|
1187
|
+
...(funding ? { usage: async ({ runtime, signal }) => {
|
|
1188
|
+
signal?.throwIfAborted(); await runtime.load()
|
|
1189
|
+
const bots = Object.values(runtime.state?.bots ?? {})
|
|
1190
|
+
const names = new Map(bots.map(bot => [bot.id, bot.name]))
|
|
1191
|
+
// An owner resolution lives on the run's package, so the receipts and
|
|
1192
|
+
// the spend total agree after a waive or an attested amount.
|
|
1193
|
+
const ledger = await funding.store.read()
|
|
1194
|
+
const resolutions = new Map()
|
|
1195
|
+
const unresolved = []
|
|
1196
|
+
for (const run of ledger.runs) {
|
|
1197
|
+
const botId = [run.modelCall, run.cliExecution, run.fundingExecution].filter(Boolean).map(value => value.botId).find(Boolean) ?? null
|
|
1198
|
+
for (const item of [...(run.packages ?? []), ...(run.reviews ?? [])]) {
|
|
1199
|
+
if (!item || item.ran !== true) continue
|
|
1200
|
+
const key = `${run.id}/${item.id}`
|
|
1201
|
+
if (item.ownerResolution) { resolutions.set(key, item.ownerResolution); continue }
|
|
1202
|
+
if (!botId || typeof item.costUsd === 'number' && Number.isFinite(item.costUsd)) continue
|
|
1203
|
+
unresolved.push({ botId, runId: run.id, packageId: item.id, provider: item.provider ?? null,
|
|
1204
|
+
model: item.model ?? null, billing: item.billing ?? null, usage: item.usage ?? null,
|
|
1205
|
+
finishedAt: item.finishedAt ?? null })
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const items = []
|
|
1209
|
+
for (const bot of bots) {
|
|
1210
|
+
let after = 0
|
|
1211
|
+
for (;;) {
|
|
1212
|
+
const page = await funding.store.modelCalls({ botId: bot.id, after, limit: 100 })
|
|
1213
|
+
for (const receipt of page.items) {
|
|
1214
|
+
const resolution = resolutions.get(`${receipt.runId}/${receipt.packageId}`) ?? null
|
|
1215
|
+
items.push({ id: receipt.id, botId: receipt.botId, botName: names.get(receipt.botId) ?? null,
|
|
1216
|
+
sessionId: receipt.sessionId, taskId: receipt.taskId, runId: receipt.runId,
|
|
1217
|
+
source: receipt.source, provider: receipt.provider, model: receipt.model, billing: receipt.funding,
|
|
1218
|
+
amountUsd: projectModelCallAmount(receipt, resolution), status: receipt.state, sequence: receipt.sequence,
|
|
1219
|
+
createdAt: receipt.createdAt, finishedAt: receipt.finishedAt,
|
|
1220
|
+
resolution: resolution ? { kind: resolution.kind, costUsd: resolution.costUsd, note: resolution.note, resolvedAt: resolution.resolvedAt } : null,
|
|
1221
|
+
inputTokens: receipt.usage?.inputTokens ?? null, outputTokens: receipt.usage?.outputTokens ?? null })
|
|
1222
|
+
}
|
|
1223
|
+
if (!page.next) break
|
|
1224
|
+
after = page.next
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const totals = funding.store.fundingTotals({ at: now() })
|
|
1228
|
+
const reserved = reservationSpending(ledger)
|
|
1229
|
+
const subscription = funding.store.fundingSubscriptionUsage({ at: now() })
|
|
1230
|
+
return { status: 'available', items, unresolved, timezone: totals.timezone, coverage: 'current-bot-source-receipts',
|
|
1231
|
+
summary: { apiUsd: totals.today, reservedUsd: reserved.reservedUsd, unknownCount: totals.unknownToday + totals.unknownMonth,
|
|
1232
|
+
subscriptionSummary: { callsToday: subscription.today.calls, callsMonth: subscription.month.calls, usdToday: totals.subscriptionToday, usdMonth: totals.subscriptionMonth } } }
|
|
1233
|
+
} } : {}) },
|
|
1234
|
+
actions: { ...actions, ...machineActions,
|
|
1235
|
+
'ui.namespace': ownerOnly(payload => uiNamespace.read(payload)),
|
|
1236
|
+
'nativeActivity.list': ownerOnly((payload, context) => {
|
|
1237
|
+
const value = nativeActivity.list(payload, context)
|
|
1238
|
+
const failures = conversationEngines?.activityHealth?.().failures ?? 0
|
|
1239
|
+
if (failures) return { ...value, recording: { status: 'degraded', failures: Math.max(failures, value.recording?.failures ?? 0), code: 'NATIVE_ACTIVITY_OBSERVER_FAILED' } }
|
|
1240
|
+
return value
|
|
1241
|
+
}),
|
|
1242
|
+
'botProfile.inspect': ownerOnly((payload, context) => inspectProductBotProfile({ runtime: product.rt, lifecycle: product.lifecycle, signal: context?.signal }, payload)),
|
|
1243
|
+
'botProfile.save': ownerOnly((payload, context) => saveProductBotProfile({ runtime: product.rt, lifecycle: product.lifecycle, signal: context?.signal }, payload, context)),
|
|
1244
|
+
'independentChat.send': ownerOnly((payload, context) => independentChatAction('send', payload, context)),
|
|
1245
|
+
'independentChat.inspect': ownerOnly((payload, context) => independentChatAction('inspect', payload, context)),
|
|
1246
|
+
'independentChat.cancel': ownerOnly((payload, context) => independentChatAction('cancel', payload, context)),
|
|
1247
|
+
'independentChat.view': ownerOnly((payload, context) => independentChatAction('view', payload, context)),
|
|
1248
|
+
'independentChat.messages': ownerOnly((payload, context) => independentChatAction('messages', payload, context)),
|
|
1249
|
+
'independentChat.newConversation': ownerOnly((payload, context) => independentChatAction('newConversation', payload, context)),
|
|
1250
|
+
// The library panel reads and writes one Bot's files. The directory is chosen by the
|
|
1251
|
+
// library itself (the caller's Bot, or the operator's choice), never by the panel.
|
|
1252
|
+
// Saved routines: list what a Bot has, and run one on a machine the caller may use.
|
|
1253
|
+
// The role catalog is a read-only convenience: it describes jobs, it grants nothing.
|
|
1254
|
+
'roles.list': async () => listRoles(),
|
|
1255
|
+
...(routineStore ? {
|
|
1256
|
+
'routine.list': async payload => routineStore.inspect(payload?.botId),
|
|
1257
|
+
'routine.save': async payload => routineStore.save(payload?.botId, payload?.routine),
|
|
1258
|
+
'routine.remove': async payload => routineStore.remove(payload?.botId, payload?.routineId),
|
|
1259
|
+
'routine.history': async payload => routineExecution.history(payload?.botId),
|
|
1260
|
+
'routine.cancel': async payload => routineExecution.cancel(payload?.botId, payload?.runId),
|
|
1261
|
+
'routine.resolve': async (payload, context) => routineExecution.resolve(payload?.botId, payload?.runId, payload?.note, context),
|
|
1262
|
+
'routine.run': async (payload, context) => routineExecution.run({ subject: taskOwner(payload),
|
|
1263
|
+
botId: payload?.botId, routineId: payload?.routineId, machineId: payload?.machineId, rootId: payload?.rootId,
|
|
1264
|
+
context, signal: context.signal }),
|
|
1265
|
+
} : {}),
|
|
1266
|
+
...(botLibrary ? {
|
|
1267
|
+
'library.list': async payload => botLibrary.list(payload?.botId, payload?.relativePath ?? ''),
|
|
1268
|
+
'library.read': async payload => ({ content: botLibrary.read(payload?.botId, payload?.relativePath ?? '') }),
|
|
1269
|
+
'library.search': async payload => ({ results: botLibrary.search(payload?.botId, payload?.query) }),
|
|
1270
|
+
'library.write': async payload => botLibrary.write(payload?.botId, payload?.relativePath ?? '', payload?.content ?? ''),
|
|
1271
|
+
'library.upload': async payload => botLibrary.upload(payload?.botId, payload?.relativePath, payload?.contentBase64, payload?.overwrite),
|
|
1272
|
+
'library.download': async payload => botLibrary.download(payload?.botId, payload?.relativePath),
|
|
1273
|
+
'library.remove': async payload => botLibrary.remove(payload?.botId, payload?.relativePath),
|
|
1274
|
+
'library.mkdir': async payload => botLibrary.mkdir(payload?.botId, payload?.relativePath),
|
|
1275
|
+
} : {}),
|
|
1276
|
+
// The Bot runtime card is a panel: its buttons dispatch on this channel, so the same four
|
|
1277
|
+
// operations the owner RPC exposes must exist here as well.
|
|
1278
|
+
...(botRuntimeAdapter ? {
|
|
1279
|
+
'botRuntimeStatus': async () => botRuntimeAdapter.list(),
|
|
1280
|
+
'botRuntimeMove': async payload => botRuntimeAdapter.move(payload?.botId, payload?.mode),
|
|
1281
|
+
'botRuntimeHistory': async payload => botRuntimeAdapter.history(payload?.botId, { limit: payload?.limit }),
|
|
1282
|
+
'botRuntimeMessage': async payload => {
|
|
1283
|
+
if (config.independentRuntime?.fullTextHistory !== true) throw new Error('HISTORY_FULLTEXT_DISABLED')
|
|
1284
|
+
return botRuntimeAdapter.readMessage(payload?.botId, payload?.index)
|
|
1285
|
+
},
|
|
1286
|
+
} : {}),
|
|
1287
|
+
// The workbench reads Bot routing through the same product channel as every other panel
|
|
1288
|
+
// action. Registering it here is what makes the routing card work at runtime; the owner
|
|
1289
|
+
// RPC methods are a second door onto the same service.
|
|
1290
|
+
'botRoutingInspect': async () => ({ ...botRouting.inspect(), settingsRevision: routingSettings.get().revision }),
|
|
1291
|
+
'botRouting.status': (payload, context) => botRouting.submissionStatus(payload, context),
|
|
1292
|
+
'botRouting.save': async (payload, context) => {
|
|
1293
|
+
const saved = routingSettings.save(payload, context)
|
|
1294
|
+
return { ...botRouting.inspect(), settingsRevision: saved.revision }
|
|
1295
|
+
},
|
|
1296
|
+
'botRoutingDecide': async payload => botRouting.decide({
|
|
1297
|
+
text: payload?.text,
|
|
1298
|
+
pinnedBotId: payload?.pinnedBotId ?? '',
|
|
1299
|
+
requiresMachine: payload?.requiresMachine === true,
|
|
1300
|
+
machineId: payload?.machineId ?? '',
|
|
1301
|
+
rootId: payload?.rootId ?? '',
|
|
1302
|
+
needs: payload?.needs ?? {},
|
|
1303
|
+
requiredTools: Array.isArray(payload?.requiredTools) ? payload.requiredTools : [],
|
|
1304
|
+
requesterBotId: typeof payload?.requesterBotId === 'string' ? payload.requesterBotId : '',
|
|
1305
|
+
}),
|
|
1306
|
+
...(botSettings ? {
|
|
1307
|
+
'binding.inspect': ownerOnly((payload, context) => inspectProductBinding({ runtime: product.rt, lifecycle: product.lifecycle, botSettings, fundingAccounts, signal: context?.signal }, payload)),
|
|
1308
|
+
'binding.save': ownerOnly(async (payload, context) => {
|
|
1309
|
+
// A Bot holds its greeting until its model is picked, and the model change releases it
|
|
1310
|
+
// immediately. Saving a binding changes both the model and its authority, so hold the
|
|
1311
|
+
// greeting across that change: otherwise the greeting turn runs before the compiled
|
|
1312
|
+
// grant carries the binding's model route and is refused as MODEL_DENIED.
|
|
1313
|
+
const bot = product.rt.botOf(payload.botId)
|
|
1314
|
+
const held = bot?.kickoff
|
|
1315
|
+
const quiet = product.lifecycle.committedMainBotIds().includes(payload.botId)
|
|
1316
|
+
&& (Object.hasOwn(payload.settings ?? {}, 'mainRoutes') || Boolean(botSettings.get(payload.botId, { publishChange: false }).settings.mainRoutes))
|
|
1317
|
+
if (bot && held) delete bot.kickoff
|
|
1318
|
+
let result
|
|
1319
|
+
let modelChanged = false
|
|
1320
|
+
try {
|
|
1321
|
+
result = await botSettings.save(payload, context)
|
|
1322
|
+
const model = result.settings?.model
|
|
1323
|
+
const ref = model ? `${model.provider}/${model.model}` : null
|
|
1324
|
+
modelChanged = Boolean(result.status === 'available' && bot && ref && bot.model !== ref)
|
|
1325
|
+
if (modelChanged) await product.rt.updateBot(payload.botId, { model: ref })
|
|
1326
|
+
await product.lifecycle.refreshGrants()
|
|
1327
|
+
} finally {
|
|
1328
|
+
if (bot && held) bot.kickoff = held
|
|
1329
|
+
}
|
|
1330
|
+
if (bot && held && quiet && modelChanged) await product.rt.save()
|
|
1331
|
+
if (bot && held && !quiet) await product.rt.updateBot(payload.botId, { model: bot.model }).catch(() => {})
|
|
1332
|
+
return result
|
|
1333
|
+
}) } : {}),
|
|
1334
|
+
...(fundingAccounts ? { 'funding.account.save': (payload, context) => fundingAccounts.save(payload, context) } : {}),
|
|
1335
|
+
// N09: owner task management runs on the owning Bot's own subject, so
|
|
1336
|
+
// the Bot's committed grant authorizes every cancel/retry/reconcile.
|
|
1337
|
+
...(taskService ? {
|
|
1338
|
+
'task.attention': ownerOnly(async payload => ({ ...await readProductAttention({ runtime: product.rt, journal, limit: TASK_READ_LIMIT }, payload),
|
|
1339
|
+
...(notifications ? { notifications: notifications.readModel() } : {}) })),
|
|
1340
|
+
// Task management is the person's own console, not a Bot surface. Every
|
|
1341
|
+
// action here names its caller class instead of inheriting the default,
|
|
1342
|
+
// so a change at the authorization seam cannot quietly hand a non-owner
|
|
1343
|
+
// the ability to cancel, retry or reconcile somebody's work.
|
|
1344
|
+
'task.inspect': ownerOnly(payload => inspectProductTask({ service: taskService, journal, subject: taskOwner(payload), taskId: payload?.taskId })),
|
|
1345
|
+
// Bot routing turns a preview into a real package: the decision names the Bot, the
|
|
1346
|
+
// worker binding is committed first, and only then is the plan submitted. A failure in
|
|
1347
|
+
// either step leaves no half-dispatched task behind.
|
|
1348
|
+
'botRouting.dispatch': (payload, context) => botRouting.dispatch(payload, context),
|
|
1349
|
+
'task.plan.submit': ownerOnly(payload => taskService.submitPlan(taskOwner(payload),{requestKey:payload?.requestKey,plan:payload?.plan},{owner:true})),
|
|
1350
|
+
'task.plan.cancel': ownerOnly(payload => taskService.cancelPlan(taskOwner(payload),{planId:payload?.planId})),
|
|
1351
|
+
'task.cancel': ownerOnly(payload => taskService.cancel(taskOwner(payload), { taskId: payload?.taskId })),
|
|
1352
|
+
'task.retry': ownerOnly(payload => taskService.retry(taskOwner(payload), { taskId: payload?.taskId, requestKey: payload?.requestKey })),
|
|
1353
|
+
'task.reconcile': ownerOnly(payload => taskService.reconcile(taskOwner(payload), { taskId: payload?.taskId, requestKey: payload?.requestKey })),
|
|
1354
|
+
} : {}),
|
|
1355
|
+
...(notifications ? {
|
|
1356
|
+
'notifications.readModel': ownerOnly(() => notifications.readModel()),
|
|
1357
|
+
'notifications.list': ownerOnly(payload => notifications.list(payload ?? {})),
|
|
1358
|
+
'notifications.read': ownerOnly(payload => notifications.read(payload?.seq)),
|
|
1359
|
+
'notifications.markAll': ownerOnly(() => notifications.markAll()),
|
|
1360
|
+
} : {}),
|
|
1361
|
+
// The adapter is constructed after createBotProduct returns. Register
|
|
1362
|
+
// lazy handlers now; checking readiness while building these options
|
|
1363
|
+
// would permanently omit every action from this product instance.
|
|
1364
|
+
'threads.readModel': ownerOnly(payload => threadAction('threads.readModel', payload)),
|
|
1365
|
+
'threads.list': ownerOnly(payload => threadAction('threads.list', payload)),
|
|
1366
|
+
'threads.create': ownerOnly(payload => threadAction('threads.create', payload)),
|
|
1367
|
+
'threads.rename': ownerOnly(payload => threadAction('threads.rename', payload)),
|
|
1368
|
+
'threads.switch': ownerOnly(payload => threadAction('threads.switch', payload)),
|
|
1369
|
+
'threads.open': ownerOnly(payload => threadAction('threads.open', payload)),
|
|
1370
|
+
'threads.archive': ownerOnly(payload => threadAction('threads.archive', payload)),
|
|
1371
|
+
'threads.unarchive': ownerOnly(payload => threadAction('threads.unarchive', payload)),
|
|
1372
|
+
'threads.ensure': ownerOnly(payload => threadAction('threads.ensure', payload)),
|
|
1373
|
+
'threads.recover': ownerOnly(payload => threadAction('threads.recover', payload)),
|
|
1374
|
+
'threads.reconcile': ownerOnly(payload => threadAction('threads.reconcile', payload)),
|
|
1375
|
+
'data.catalog': ownerOnly(payload => dataAction('data.catalog', payload)),
|
|
1376
|
+
'data.load': ownerOnly(payload => dataAction('data.load', payload)),
|
|
1377
|
+
'data.describe': ownerOnly(payload => dataAction('data.describe', payload)),
|
|
1378
|
+
'data.sql': ownerOnly(payload => dataAction('data.sql', payload)),
|
|
1379
|
+
'data.show': ownerOnly(payload => dataAction('data.show', payload)),
|
|
1380
|
+
'data.page': ownerOnly(payload => dataAction('data.page', payload)),
|
|
1381
|
+
'data.export': ownerOnly(payload => dataAction('data.export', payload)),
|
|
1382
|
+
'data.remove': ownerOnly(payload => dataAction('data.remove', payload)),
|
|
1383
|
+
'data.cancel': ownerOnly(payload => dataAction('data.cancel', payload)),
|
|
1384
|
+
// Pinned web panels are the person's own console, not a Bot surface. Every entry
|
|
1385
|
+
// names its caller class instead of inheriting whatever the seam defaults to, and
|
|
1386
|
+
// none of them accepts a URL from the payload: the probe reads the owner's stored
|
|
1387
|
+
// pin, so there is no "fetch this address" door here at all.
|
|
1388
|
+
'webPanels.list': ownerOnly((payload, context) => webPanelActions.actions['webPanels.list'](payload, context)),
|
|
1389
|
+
'webPanels.probe': ownerOnly((payload, context) => webPanelActions.actions['webPanels.probe'](payload, context)),
|
|
1390
|
+
'webPanels.save': ownerOnly((payload, context) => webPanelActions.actions['webPanels.save'](payload, context)),
|
|
1391
|
+
'webPanels.remove': ownerOnly((payload, context) => webPanelActions.actions['webPanels.remove'](payload, context)),
|
|
1392
|
+
'webPanels.order': ownerOnly((payload, context) => webPanelActions.actions['webPanels.order'](payload, context)),
|
|
1393
|
+
// An unconfirmed receipt fail-closes its Bot until an owner resolves it:
|
|
1394
|
+
// attest the vendor-confirmed amount or waive it, with a required note.
|
|
1395
|
+
...(funding ? { 'usage.resolve': (payload, context) => {
|
|
1396
|
+
if (context?.kind !== 'owner') throw new Error('REMOTE_OWNER_REQUIRED')
|
|
1397
|
+
if (!payload || typeof payload !== 'object' || Object.keys(payload).some(key => !['botId', 'runId', 'packageId', 'kind', 'costUsd', 'note'].includes(key))) {
|
|
1398
|
+
throw new Error('INVALID_RESOLUTION_REQUEST')
|
|
1399
|
+
}
|
|
1400
|
+
return funding.store.resolveUnknownPackage(payload)
|
|
1401
|
+
} } : {}) },
|
|
1402
|
+
authorizeRequest })
|
|
1403
|
+
const preparedAuxiliary = createAuxiliaryModelStream({
|
|
1404
|
+
llm: () => { try { return ctx.get?.('llm') ?? ctx.llm ?? null } catch { return ctx.llm ?? null } },
|
|
1405
|
+
frames: modelFrames, fundingFor: bindingFunding,
|
|
1406
|
+
identityFor: request => policy.modelIdentity(request),
|
|
1407
|
+
redact: text => product.rt.redactSecrets(text),
|
|
1408
|
+
carryUsageTag: (from, to) => product.rt.carryUsageTag(from, to),
|
|
1409
|
+
})
|
|
1410
|
+
const auxiliaryStream = async function* (request, context) {
|
|
1411
|
+
await product.rt.loadSecrets?.()
|
|
1412
|
+
yield* preparedAuxiliary(request, context)
|
|
1413
|
+
}
|
|
1414
|
+
auxiliaryStream.stream = auxiliaryStream
|
|
1415
|
+
auxiliaryStream.reasoningOff = preparedAuxiliary.reasoningOff
|
|
1416
|
+
ctx.provide('botAuxiliaryModelStream', Object.freeze(auxiliaryStream))
|
|
1417
|
+
// Fix the initial history boundary after committed Bots are hydrated,
|
|
1418
|
+
// before tools can start new work. A failed read fails startup visibly.
|
|
1419
|
+
await product.rt.load()
|
|
1420
|
+
independentChat = createIndependentChatService({ Database, file: storageFile, lifecycle: product.lifecycle,
|
|
1421
|
+
runtime: () => { try { return ctx.get?.('botRuntimeAdapter') ?? null } catch { return null } }, now })
|
|
1422
|
+
ctx.provide('botIndependentChat', independentChat)
|
|
1423
|
+
notifications?.readModel()
|
|
1424
|
+
// Per-Bot conversation threads. The catalog is this Host's own file
|
|
1425
|
+
// beside the rest of its state. A thread Session is made through the
|
|
1426
|
+
// product runtime and recorded in the Bot's committed state, which is
|
|
1427
|
+
// what lets the lifecycle compile it into the Bot's ownership — see
|
|
1428
|
+
// bot-thread-host.mjs. It is built after the runtime exists, because the
|
|
1429
|
+
// adapter asks the runtime for its own facts.
|
|
1430
|
+
botThreads = createBotThreadHost({ rt: product.rt, lifecycle: product.lifecycle, authority, directory,
|
|
1431
|
+
file: join(dirname(storageFile), 'bot-threads.json'), runtimeMode: botId => product.serving.readMode(botId) ?? 'unavailable' })
|
|
1432
|
+
ctx.provide('botThreads', botThreads)
|
|
1433
|
+
botThreads?.service.reconcile()
|
|
1434
|
+
teamCoordination = createTeamCoordination({ Database, file: storageFile, rt: product.rt,
|
|
1435
|
+
directory, authority, lifecycle: product.lifecycle, serving: product.serving, policy,
|
|
1436
|
+
threadHost: () => botThreads,
|
|
1437
|
+
runtime: () => ctx.get('botRuntimeAdapter'), flush: session => ctx.sessions.flush(session),
|
|
1438
|
+
sessionEvents: { subscribe: handler => ctx.on('session/event', handler) }, now, limits: { maxDepth: 4 } })
|
|
1439
|
+
ctx.provide('botTeamCoordination', teamCoordination)
|
|
1440
|
+
releaseTeamGuard = ctx.tools.guard(exec => {
|
|
1441
|
+
const botId = (() => { try { return describeBot(directory.forAgent(exec.agent)) } catch { return null } })()
|
|
1442
|
+
if (!botId) return
|
|
1443
|
+
try { teamCoordination.assertCanAct(botId, { sessionId: exec.agent.session.id }) }
|
|
1444
|
+
catch (error) { return `${error.code ?? 'TEAM_ACCESS_CHANGED'}: ${product.rt.redactSecrets(String(error.message))}` }
|
|
1445
|
+
})
|
|
1446
|
+
releaseTeamTools = registerTeamTools(ctx, { service: teamCoordination,
|
|
1447
|
+
botIdFor: exec => describeBot(directory.forAgent(exec?.agent)),
|
|
1448
|
+
names: ['coordinate_bots', 'assignment_status', 'list_threads'], redact: product.rt.redactSecrets })
|
|
1449
|
+
releaseTeamStop = ctx.on('session/event', (session, event) => {
|
|
1450
|
+
if (event.type !== 'turn/end' || event.data?.reason?.kind !== 'aborted'
|
|
1451
|
+
|| !['user', 'parent'].includes(event.data.reason.reason?.kind)) return
|
|
1452
|
+
let botId
|
|
1453
|
+
try { botId = describeBot(directory.forSession(session.id)) } catch { return }
|
|
1454
|
+
if (botId) teamCoordination.stop(botId)
|
|
1455
|
+
})
|
|
1456
|
+
teamCoordination.start()
|
|
1457
|
+
releaseMachineSecrets=guardMachineSecrets(ctx,product.rt,machines)
|
|
1458
|
+
ctx.provide('botRemoteMachines', machines)
|
|
1459
|
+
registerRemoteComputerTools(ctx,{runtime:product.rt,directory,machines,taskService,remoteCliBackend,operatorSubject,botRouting,routineStore,routineExecution,handoffTask,
|
|
1460
|
+
routedExecution: turnBackends, botIdFor: exec => describeBot(directory.forAgent(exec?.agent)), routingBotIds: () => product.lifecycle.committedMainBotIds() })
|
|
1461
|
+
releaseDataTools = registerDataTools(ctx, { engine: dataEngine, botIdFor: dataAccess.botIdFor })
|
|
1462
|
+
// The MR execution paths project their routes through the same Bot
|
|
1463
|
+
// binding, so a consult run carries the binding price instead of an
|
|
1464
|
+
// unknown-cost package.
|
|
1465
|
+
if (funding) ctx.provide('botFunding', funding.funded)
|
|
1466
|
+
deferredRouterState?.resolve(funding.state)
|
|
1467
|
+
machineHeartbeat = createMachineHeartbeat({ machines: () => machines, preferredBotIds: () => product.lifecycle.committedMainBotIds() })
|
|
1468
|
+
machineHeartbeat.start()
|
|
1469
|
+
timer = setInterval(() => { void refresh() }, refreshIntervalMs)
|
|
1470
|
+
timer.unref()
|
|
1471
|
+
return product
|
|
1472
|
+
}catch(error){
|
|
1473
|
+
await grantRefresh.close()
|
|
1474
|
+
ctx.get?.('botIndependentToolHost')?.close?.()
|
|
1475
|
+
try { independentChat?.close() } catch { /* remaining Host teardown must still run */ }
|
|
1476
|
+
try { teamCoordination?.close() } catch {}
|
|
1477
|
+
releaseTeamTools?.(); releaseTeamGuard?.(); releaseTeamStop?.()
|
|
1478
|
+
await conversationEngines?.close().catch(() => {})
|
|
1479
|
+
try { nativeActivity?.close() } catch {}
|
|
1480
|
+
await machineHeartbeat?.close?.()
|
|
1481
|
+
await routineExecution?.close?.()
|
|
1482
|
+
await machines?.closeDesktopSessions?.()
|
|
1483
|
+
await desktopProxy?.close()
|
|
1484
|
+
desktopHttp?.close()
|
|
1485
|
+
releaseDataTools?.()
|
|
1486
|
+
await dataEngine?.closeAll().catch(() => {})
|
|
1487
|
+
for(const cleanup of [()=>product?.dispose(),()=>releaseMachineSecrets?.(),()=>releaseTaskTools?.(),()=>securePairing?.close(),()=>nativeRemoteActions?.close(),()=>machines?.close(),()=>botSettings?.close(),()=>fundingAccounts?.close(),()=>modelAccounting?.close(),()=>routingSettings?.close(),()=>uiNamespace?.close(),()=>webPanelStore?.close()])try{cleanup()}catch{}
|
|
1488
|
+
if(taskRunnerWatchdog)clearInterval(taskRunnerWatchdog)
|
|
1489
|
+
if(taskService)await taskService.close().catch(()=>{})
|
|
1490
|
+
else journal?.close()
|
|
1491
|
+
notifications?.close()
|
|
1492
|
+
botThreads?.close()
|
|
1493
|
+
turnBackends?.close()
|
|
1494
|
+
if(remoteCliBackend)await remoteCliBackend.close().catch(()=>{})
|
|
1495
|
+
if(funding)await funding.close().catch(()=>{})
|
|
1496
|
+
if(deferredRouterState)try{deferredRouterState.reject(error)}catch{}
|
|
1497
|
+
if(sharedAdmission)await sharedAdmission.release().catch(()=>{})
|
|
1498
|
+
throw error
|
|
1499
|
+
}
|
|
1500
|
+
})()
|
|
1501
|
+
if (failClosed) {
|
|
1502
|
+
// A half-composed product (no Bot runtime) must never look like a working Host.
|
|
1503
|
+
boot.catch(error => {
|
|
1504
|
+
process.stderr.write(`Bot product composition failed: ${error?.stack ?? error}\n`)
|
|
1505
|
+
setTimeout(() => { throw error }, 0)
|
|
1506
|
+
})
|
|
1507
|
+
}
|
|
1508
|
+
return Object.freeze({
|
|
1509
|
+
machineId, workspaceId, grants, grantFor,
|
|
1510
|
+
observations: () => accountStream.observations?.() ?? [],
|
|
1511
|
+
routeStatus: () => ({ hydrated, routes: [...routes].sort() }),
|
|
1512
|
+
boot,
|
|
1513
|
+
async dispose() {
|
|
1514
|
+
if (timer) clearInterval(timer)
|
|
1515
|
+
const product = await boot.then(value => value, () => null)
|
|
1516
|
+
await grantRefresh.close()
|
|
1517
|
+
ctx.get?.('botIndependentToolHost')?.close?.()
|
|
1518
|
+
try { independentChat?.close() } catch { /* remaining Host teardown must still run */ }
|
|
1519
|
+
try { teamCoordination?.close() } catch {}
|
|
1520
|
+
releaseTeamTools?.(); releaseTeamGuard?.(); releaseTeamStop?.()
|
|
1521
|
+
let engineCloseError
|
|
1522
|
+
try { await conversationEngines?.close() } catch (error) { engineCloseError = error }
|
|
1523
|
+
try { nativeActivity?.close() } catch (error) { engineCloseError ??= error }
|
|
1524
|
+
try{
|
|
1525
|
+
await machineHeartbeat?.close?.()
|
|
1526
|
+
await routineExecution?.close?.()
|
|
1527
|
+
await machines?.closeDesktopSessions?.()
|
|
1528
|
+
await desktopProxy?.close()
|
|
1529
|
+
desktopHttp?.close()
|
|
1530
|
+
releaseDataTools?.()
|
|
1531
|
+
await dataEngine?.closeAll().catch(() => {})
|
|
1532
|
+
for(const cleanup of [()=>product?.dispose(),()=>releaseMachineSecrets?.(),()=>releaseTaskTools?.(),()=>securePairing?.close(),()=>nativeRemoteActions?.close(),()=>machines?.close(),()=>botSettings?.close(),()=>fundingAccounts?.close(),()=>modelAccounting?.close(),()=>routingSettings?.close(),()=>uiNamespace?.close(),()=>webPanelStore?.close()])try{cleanup()}catch{}
|
|
1533
|
+
if(taskRunnerWatchdog)clearInterval(taskRunnerWatchdog)
|
|
1534
|
+
if(taskService)await taskService.close().catch(()=>{})
|
|
1535
|
+
else journal?.close()
|
|
1536
|
+
notifications?.close()
|
|
1537
|
+
botThreads?.close()
|
|
1538
|
+
turnBackends?.close()
|
|
1539
|
+
if(remoteCliBackend)await remoteCliBackend.close().catch(()=>{})
|
|
1540
|
+
if(funding)await funding.close()
|
|
1541
|
+
}finally{authority.dispose();if(sharedAdmission)await sharedAdmission.release()}
|
|
1542
|
+
if (engineCloseError) throw engineCloseError
|
|
1543
|
+
},
|
|
1544
|
+
})
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** Bounded diagnostic decorator: identity and source only, never a monetary claim. */
|
|
1548
|
+
export function createDiagnosticAccountStream({ limit = OBSERVATION_LIMIT, now = Date.now } = {}) {
|
|
1549
|
+
const ring = []
|
|
1550
|
+
const stream = (options, next, meta) => {
|
|
1551
|
+
if (ring.length >= limit) ring.shift()
|
|
1552
|
+
ring.push({ at: now(), source: meta?.source ?? null, sessionId: meta?.sessionId ?? null,
|
|
1553
|
+
botId: meta?.botId ?? null, provider: options?.provider ?? null, model: options?.model ?? null,
|
|
1554
|
+
resolved: Boolean(meta?.subject) })
|
|
1555
|
+
return next()
|
|
1556
|
+
}
|
|
1557
|
+
stream.observations = () => ring.slice()
|
|
1558
|
+
return stream
|
|
1559
|
+
}
|