@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isAbsolute, join } from 'node:path'
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { createClaudeLlmAdapter } from './claude-llm-adapter.mjs'
|
|
4
|
+
|
|
5
|
+
const fail = code => { throw Object.assign(new Error(code), { code }) }
|
|
6
|
+
const plain = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
const keys = (value, names) => { if (!plain(value) || Object.keys(value).some(key => !names.includes(key))) fail('CONVERSATION_ENGINE_CONFIG_INVALID') }
|
|
8
|
+
const id = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,100}$/.test(value)
|
|
9
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
|
|
10
|
+
|
|
11
|
+
/** Host configuration, never a Bot binding or model/request payload. */
|
|
12
|
+
export function normalizeConversationEngines(value = []) {
|
|
13
|
+
if (!Array.isArray(value) || value.length > 32) fail('CONVERSATION_ENGINE_CONFIG_INVALID')
|
|
14
|
+
const profiles = value.map(item => {
|
|
15
|
+
keys(item, ['id', 'driver', 'provider', 'model', 'name', 'entry', 'credentialRef', 'deadlineMs', 'hostTools'])
|
|
16
|
+
if (!id(item.id) || item.driver !== 'claude' || !id(item.provider)
|
|
17
|
+
|| typeof item.model !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/.test(item.model)) fail('CONVERSATION_ENGINE_CONFIG_INVALID')
|
|
18
|
+
keys(item.entry, ['executable', 'sha256', 'version', 'script', 'scriptSha256'])
|
|
19
|
+
const entry = item.entry
|
|
20
|
+
if (!isAbsolute(entry.executable ?? '') || !hash(entry.sha256) || typeof entry.version !== 'string'
|
|
21
|
+
|| !/^[0-9][A-Za-z0-9.+_-]{0,31}$/.test(entry.version)) fail('CONVERSATION_ENGINE_ENTRY_INVALID')
|
|
22
|
+
if (entry.script !== undefined && (!isAbsolute(entry.script) || !hash(entry.scriptSha256))
|
|
23
|
+
|| entry.script === undefined && entry.scriptSha256 !== undefined) fail('CONVERSATION_ENGINE_ENTRY_INVALID')
|
|
24
|
+
if (typeof item.credentialRef !== 'string' || !/^ds-secret\/[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(item.credentialRef)) fail('CONVERSATION_ENGINE_CREDENTIAL_REF_INVALID')
|
|
25
|
+
const deadlineMs = item.deadlineMs ?? 1_800_000
|
|
26
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1000 || deadlineMs > 7_200_000) fail('CONVERSATION_ENGINE_DEADLINE_INVALID')
|
|
27
|
+
if (item.name !== undefined && (typeof item.name !== 'string' || !item.name.trim() || item.name.length > 128)) fail('CONVERSATION_ENGINE_CONFIG_INVALID')
|
|
28
|
+
if (item.hostTools !== undefined && typeof item.hostTools !== 'boolean') fail('CONVERSATION_ENGINE_CONFIG_INVALID')
|
|
29
|
+
return Object.freeze({ id: item.id, driver: item.driver, provider: item.provider, model: item.model,
|
|
30
|
+
name: item.name ?? 'Claude(订阅会话)', entry: Object.freeze({ ...entry }), credentialRef: item.credentialRef, deadlineMs,
|
|
31
|
+
hostTools: item.hostTools === true })
|
|
32
|
+
})
|
|
33
|
+
for (const field of ['id', 'provider']) if (new Set(profiles.map(profile => profile[field])).size !== profiles.length) fail('CONVERSATION_ENGINE_DUPLICATE')
|
|
34
|
+
return Object.freeze(profiles)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Copy an explicitly selected Host-only opaque CLI credential into a private home.
|
|
38
|
+
* No ambient account discovery, model arguments or credentials enter argv/catalogs.
|
|
39
|
+
*/
|
|
40
|
+
export function validateClaudeProfileCredential(value) {
|
|
41
|
+
if (typeof value !== 'string' || Buffer.byteLength(value) > 128_000) fail('CONVERSATION_ENGINE_CREDENTIAL_INVALID')
|
|
42
|
+
let parsed
|
|
43
|
+
try { parsed = JSON.parse(value) } catch { fail('CONVERSATION_ENGINE_CREDENTIAL_INVALID') }
|
|
44
|
+
if (!plain(parsed) || !Object.keys(parsed).length) fail('CONVERSATION_ENGINE_CREDENTIAL_INVALID')
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function provisionClaudeProfile(home, profile, resolveSecret) {
|
|
49
|
+
if (!isAbsolute(home ?? '') || typeof resolveSecret !== 'function') fail('CONVERSATION_ENGINE_CREDENTIAL_UNAVAILABLE')
|
|
50
|
+
const value = validateClaudeProfileCredential(await resolveSecret(profile.credentialRef, profile))
|
|
51
|
+
const dir = join(home, '.claude')
|
|
52
|
+
await mkdir(dir, { recursive: true, mode: 0o700 })
|
|
53
|
+
await writeFile(join(dir, '.credentials.json'), value, { mode: 0o600 })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Mount explicitly configured text-only routes into the Host's public model catalog.
|
|
57
|
+
* Registration starts no native process; the existing model waterfall owns all money.
|
|
58
|
+
*/
|
|
59
|
+
export async function mountConversationEngines({ llm, profiles = [], runtimeRoot, identityFor, fundingFor, resolveSecret, requestFor, toolsFor, onActivity } = {}) {
|
|
60
|
+
const registrations = []
|
|
61
|
+
if (profiles.length && (typeof llm?.registerAdapter !== 'function' || !isAbsolute(runtimeRoot ?? '')
|
|
62
|
+
|| typeof identityFor !== 'function' || typeof fundingFor !== 'function' || typeof resolveSecret !== 'function')) fail('CONVERSATION_ENGINE_HOST_REQUIRED')
|
|
63
|
+
const close = async () => {
|
|
64
|
+
const failures = []
|
|
65
|
+
for (const item of registrations.splice(0).reverse()) {
|
|
66
|
+
try { await item.adapter.close() } catch (error) { failures.push(error) }
|
|
67
|
+
try { item.release() } catch (error) { failures.push(error) }
|
|
68
|
+
}
|
|
69
|
+
if (failures.length) throw new AggregateError(failures, 'CONVERSATION_ENGINE_CLOSE_FAILED')
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
for (const profile of profiles) {
|
|
73
|
+
if (profile.hostTools && typeof toolsFor !== 'function') fail('CONVERSATION_ENGINE_TOOL_HOST_REQUIRED')
|
|
74
|
+
const adapter = createClaudeLlmAdapter({ provider: profile.provider, model: profile.model, name: profile.name,
|
|
75
|
+
funding: 'subscription', fundingFor: route => fundingFor(profile, route),
|
|
76
|
+
entry: profile.entry, runtimeRoot: join(runtimeRoot, profile.id), deadlineMs: profile.deadlineMs,
|
|
77
|
+
workspaceFor: async () => join(runtimeRoot, profile.id, 'workspace'),
|
|
78
|
+
identityFor: route => identityFor(profile, route),
|
|
79
|
+
...(requestFor ? { requestFor: (request, identity) => requestFor(profile, request, identity) } : {}),
|
|
80
|
+
...(profile.hostTools ? { toolsFor: identity => toolsFor(profile, identity) } : {}),
|
|
81
|
+
...(onActivity ? { onActivity: event => onActivity(profile, event) } : {}),
|
|
82
|
+
provisionCredentials: (home, identity) => provisionClaudeProfile(home, profile, (ref, current) => resolveSecret(ref, current, identity)),
|
|
83
|
+
tools: [], handlers: [], permissionMode: 'default' })
|
|
84
|
+
await mkdir(join(runtimeRoot, profile.id, 'workspace'), { recursive: true, mode: 0o700 })
|
|
85
|
+
try {
|
|
86
|
+
const release = llm.registerAdapter([profile.provider], adapter)
|
|
87
|
+
registrations.push({ profileId: profile.id, adapter, release })
|
|
88
|
+
} catch (error) { await adapter.close(); throw error }
|
|
89
|
+
}
|
|
90
|
+
} catch (error) { await close(); throw error }
|
|
91
|
+
return Object.freeze({ close,
|
|
92
|
+
activityHealth: () => ({ failures: registrations.reduce((total, item) => total + (item.adapter.snapshot().activityObserverFailures ?? 0), 0) }),
|
|
93
|
+
invalidateBot: (profileId, botId, reason) => registrations.find(item => item.profileId === profileId)?.adapter.invalidateBot(botId, reason),
|
|
94
|
+
async retireBot(botId, reason = 'new-conversation') {
|
|
95
|
+
const retired = await Promise.all(registrations.map(item => item.adapter.invalidateBot(botId, reason)))
|
|
96
|
+
const count = retired.reduce((total, item) => total + item.retired, 0), evidence = retired.flatMap(item => item.evidence)
|
|
97
|
+
if (count === 0) return { notStarted: true, possiblyApplied: false }
|
|
98
|
+
if (evidence.length !== count || evidence.some(item => !item || item.possiblyApplied === true || item.status === 'unknown'
|
|
99
|
+
|| !(item.treeStopped === true || item.notStarted === true && item.possiblyApplied === false))) return { status: 'unknown', treeStopped: false }
|
|
100
|
+
return evidence.every(item => item.notStarted === true) ? { notStarted: true, possiblyApplied: false } : { treeStopped: true }
|
|
101
|
+
},
|
|
102
|
+
catalog: () => profiles.map(profile => ({ id: profile.id, driver: profile.driver, provider: profile.provider,
|
|
103
|
+
model: profile.model, name: profile.name, funding: 'subscription', input: 'text', hostTools: profile.hostTools, status: 'configured' })) })
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -5,17 +5,43 @@ import { isAbsolute, join } from 'node:path'
|
|
|
5
5
|
import { performance } from 'node:perf_hooks'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { PolicyError } from './authority.mjs'
|
|
8
|
+
import { bridgeResponseFrames, normalizeBridgeMcpResult } from './cli-bridge-response.mjs'
|
|
8
9
|
import { nativeProcess, resolveNativeHelper } from '../../cli-runtime/src/native-process.mjs'
|
|
9
10
|
|
|
10
11
|
const BRIDGE = fileURLToPath(new URL('./cli-stdio-bridge.mjs', import.meta.url))
|
|
11
12
|
const MAX_BYTES = 262_144
|
|
13
|
+
const REQUEST_KEYS = ['type', 'id', 'op', 'name', 'args']
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A trusted handler may run under an authority name that differs from the MCP
|
|
17
|
+
* tool name the native client sees. The wire name never reaches the lease, and
|
|
18
|
+
* a handler without one is exactly its definition name.
|
|
19
|
+
*/
|
|
20
|
+
const capabilityOf = handler => {
|
|
21
|
+
const value = handler.capability ?? handler.definition.name
|
|
22
|
+
if (typeof value !== 'string' || !value || Buffer.byteLength(value) > 256) throw new TypeError('INVALID_HANDLER_CAPABILITY')
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
/** CLI-grandchild handoff: public signed locator, OS peer identity, host-only capability lease. */
|
|
14
|
-
export async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 30_000, helper }) {
|
|
27
|
+
export async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 30_000, helper, contextFor = null }) {
|
|
15
28
|
if (!isAbsolute(home)) throw new TypeError('An isolated absolute home is required')
|
|
29
|
+
// Host-owned per-turn context. The Host, never the native client, decides
|
|
30
|
+
// whether this receiver is inside a live turn.
|
|
31
|
+
if (contextFor !== null && typeof contextFor !== 'function') throw new TypeError('INVALID_HOST_CONTEXT_CALLBACK')
|
|
16
32
|
const ownedBinding = Object.freeze({ ...binding })
|
|
17
33
|
const registry = new Map(handlers.map(handler => [handler.definition.name, handler]))
|
|
18
34
|
if (registry.size !== handlers.length || tools.some(name => !registry.has(name))) throw new TypeError('Unknown or duplicate bridge handler')
|
|
35
|
+
// Selected wire names, each paired with the authority name it runs under.
|
|
36
|
+
// Unselected handlers stay inert: no capability, no definition, no execution.
|
|
37
|
+
const selected = new Map(tools.map(name => [name, { handler: registry.get(name), capability: capabilityOf(registry.get(name)) }]))
|
|
38
|
+
const leaseTools = [...new Set([...selected.values()].map(entry => entry.capability))]
|
|
39
|
+
const byCapability = new Map()
|
|
40
|
+
for (const entry of selected.values()) {
|
|
41
|
+
const sharing = byCapability.get(entry.capability)
|
|
42
|
+
if (sharing) sharing.push(entry.handler)
|
|
43
|
+
else byCapability.set(entry.capability, [entry.handler])
|
|
44
|
+
}
|
|
19
45
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 7_200_000 || typeof authority.renew !== 'function') throw new TypeError('BOUNDED_RENEWABLE_LEASE_REQUIRED')
|
|
20
46
|
const leaseTtlMs = Math.min(30_000, ttlMs)
|
|
21
47
|
const startedAt = performance.now()
|
|
@@ -42,17 +68,38 @@ export async function createCliBootstrap({ authority, subject, binding, tools, h
|
|
|
42
68
|
if(Buffer.byteLength(endpoint)>103){await rmdir(socketDirectory);throw new Error('PRIVATE_SOCKET_PATH_TOO_LONG')}
|
|
43
69
|
}
|
|
44
70
|
let lease
|
|
45
|
-
try{lease=authority.issue(subject,{binding:ownedBinding,tools,ttlMs:leaseTtlMs})}
|
|
71
|
+
try{lease=authority.issue(subject,{binding:ownedBinding,tools:leaseTools,ttlMs:leaseTtlMs})}
|
|
46
72
|
catch(error){if(socketDirectory)await rmdir(socketDirectory);throw error}
|
|
47
73
|
let relay, closed = false, bound = false, connected = false, closing
|
|
48
|
-
let accepted, rejected = 0, requests = 0, replies = 0
|
|
74
|
+
let accepted, rejected = 0, requests = 0, replies = 0, highestRequestId = 0
|
|
49
75
|
const active = new Map()
|
|
50
76
|
const release = () => { authority.release(lease.token); for (const controller of active.values()) controller.abort(new Error('BRIDGE_CLOSED')) }
|
|
51
|
-
const reply = (id, value) => {
|
|
77
|
+
const reply = async (id, value, signal) => {
|
|
52
78
|
if (!relay || relay.closed) return
|
|
53
79
|
replies++
|
|
54
|
-
const
|
|
55
|
-
|
|
80
|
+
for (const frame of bridgeResponseFrames(id, value)) {
|
|
81
|
+
signal?.throwIfAborted()
|
|
82
|
+
await relay.send(frame)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Host turn context, read synchronously after the structural and replay
|
|
87
|
+
* guards. The Host owns the returned object; nothing in it can widen the
|
|
88
|
+
* binding, the lease or the final signal the handler observes.
|
|
89
|
+
*/
|
|
90
|
+
const hostContext = (op, name, id) => {
|
|
91
|
+
if (!contextFor) return null
|
|
92
|
+
let value
|
|
93
|
+
try { value = contextFor({ op, name, id }) }
|
|
94
|
+
catch (error) { throw error instanceof PolicyError ? error : new PolicyError('CONTEXT_UNAVAILABLE', { gate: 'host-context' }) }
|
|
95
|
+
if (!value || typeof value !== 'object' || typeof value.then === 'function') throw new PolicyError('CONTEXT_UNAVAILABLE', { gate: 'host-context' })
|
|
96
|
+
return value
|
|
97
|
+
}
|
|
98
|
+
/** The receiver's own cancel signal combined with the Host turn signal. */
|
|
99
|
+
const requestSignal = (controller, context) => {
|
|
100
|
+
const signal = context?.signal ?? null
|
|
101
|
+
if (signal !== null && !(signal instanceof AbortSignal)) throw new PolicyError('CONTEXT_UNAVAILABLE', { gate: 'host-signal' })
|
|
102
|
+
return signal ? AbortSignal.any([controller.signal, signal]) : controller.signal
|
|
56
103
|
}
|
|
57
104
|
async function request(message) {
|
|
58
105
|
requests++
|
|
@@ -60,21 +107,44 @@ export async function createCliBootstrap({ authority, subject, binding, tools, h
|
|
|
60
107
|
if (message.type === 'cancel') { active.get(message.id)?.abort(new Error('REQUEST_CANCELLED')); return }
|
|
61
108
|
if (message.type !== 'request') return
|
|
62
109
|
const id = message.id
|
|
110
|
+
// One bounded receiver-side counter instead of a set of seen ids: a bridge
|
|
111
|
+
// client numbers its requests strictly upward, so anything at or below the
|
|
112
|
+
// highest one already ran or is running and is never run again. The check
|
|
113
|
+
// sits before every await, so same-tick and out-of-order replays are
|
|
114
|
+
// refused before a handler starts. A cancel may still name an active id.
|
|
115
|
+
if (id <= highestRequestId) { void reply(id, { ok: false, code: 'DUPLICATE_REQUEST' }).catch(() => {}); return }
|
|
116
|
+
highestRequestId = id
|
|
63
117
|
try {
|
|
64
118
|
if (closed) throw new PolicyError('BRIDGE_CLOSED')
|
|
65
|
-
if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some(key => !
|
|
66
|
-
|
|
67
|
-
|
|
119
|
+
if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some(key => !REQUEST_KEYS.includes(key))) throw new PolicyError('INVALID_REQUEST')
|
|
120
|
+
// The replay guard already refuses an id that is at or below the highest
|
|
121
|
+
// one, so an active id can never re-enter here.
|
|
122
|
+
if (active.size >= 16) throw new PolicyError('BUSY')
|
|
123
|
+
if (message.op === 'list') {
|
|
124
|
+
const context = hostContext('list', null, id)
|
|
125
|
+
requestSignal(new AbortController(), context).throwIfAborted()
|
|
126
|
+
await reply(id, { ok: true, value: authority.list(lease.token, ownedBinding).flatMap(capability => byCapability.get(capability) ?? []).map(handler => handler.definition) })
|
|
127
|
+
return
|
|
128
|
+
}
|
|
68
129
|
if (message.op !== 'call') throw new PolicyError('INVALID_REQUEST')
|
|
69
|
-
const
|
|
130
|
+
const entry = selected.get(message.name)
|
|
131
|
+
if (!entry) throw new PolicyError('TOOL_DENIED')
|
|
132
|
+
const context = hostContext('call', message.name, id)
|
|
133
|
+
const controller = new AbortController()
|
|
134
|
+
const signal = requestSignal(controller, context)
|
|
135
|
+
active.set(id, controller)
|
|
70
136
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
137
|
+
signal.throwIfAborted()
|
|
138
|
+
const value = await authority.run(lease.token, ownedBinding, entry.capability, ({ signal: authoritySignal }) => {
|
|
139
|
+
authoritySignal.throwIfAborted()
|
|
140
|
+
return entry.handler.execute(entry.handler.parse(message.args), { signal: authoritySignal, binding: ownedBinding, context })
|
|
141
|
+
}, { signal })
|
|
142
|
+
signal.throwIfAborted()
|
|
143
|
+
const mcpResult = entry.handler.render ? normalizeBridgeMcpResult(await entry.handler.render(value)) : null
|
|
144
|
+
signal.throwIfAborted()
|
|
145
|
+
await reply(id, { ok: true, ...(mcpResult ? { mcpResult } : { value }) }, signal)
|
|
76
146
|
} finally { active.delete(id) }
|
|
77
|
-
} catch (error) { reply(id, { ok: false, code: error instanceof PolicyError ? error.code : 'HANDLER_REFUSED' }) }
|
|
147
|
+
} catch (error) { await reply(id, { ok: false, code: error instanceof PolicyError ? error.code : 'HANDLER_REFUSED' }).catch(() => {}) }
|
|
78
148
|
}
|
|
79
149
|
const close = () => {
|
|
80
150
|
if (closing) return closing
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const CHUNK_BYTES = 128 * 1024
|
|
4
|
+
const MAX_RESPONSE = 9 * 1024 * 1024
|
|
5
|
+
const MAX_BUFFERED = 16 * 1024 * 1024
|
|
6
|
+
const fail = () => { throw new Error('INVALID_BRIDGE_RESPONSE') }
|
|
7
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex')
|
|
8
|
+
|
|
9
|
+
/** Large MCP pictures cross the existing bounded native pipe in small frames. */
|
|
10
|
+
export function* bridgeResponseFrames(id, value) {
|
|
11
|
+
let bytes = Buffer.from(JSON.stringify({ type: 'response', id, ...value }))
|
|
12
|
+
if (bytes.length > MAX_RESPONSE) bytes = Buffer.from(JSON.stringify({ type: 'response', id, ok: false, code: 'OUTPUT_LIMIT' }))
|
|
13
|
+
if (bytes.length <= CHUNK_BYTES) { yield JSON.parse(bytes.toString('utf8')); return }
|
|
14
|
+
yield { type: 'response-start', id, bytes: bytes.length }
|
|
15
|
+
for (let offset = 0, index = 0; offset < bytes.length; offset += CHUNK_BYTES, index++) {
|
|
16
|
+
yield { type: 'response-chunk', id, index, data: bytes.subarray(offset, offset + CHUNK_BYTES).toString('base64') }
|
|
17
|
+
}
|
|
18
|
+
yield { type: 'response-end', id, sha256: sha(bytes) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Reassembly is bounded across all pending calls and releases on cancellation. */
|
|
22
|
+
export class BridgeResponseReader {
|
|
23
|
+
#parts = new Map()
|
|
24
|
+
#buffered = 0
|
|
25
|
+
|
|
26
|
+
drop(id) {
|
|
27
|
+
const state = this.#parts.get(id)
|
|
28
|
+
if (state) this.#buffered -= state.size
|
|
29
|
+
this.#parts.delete(id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear() { this.#parts.clear(); this.#buffered = 0 }
|
|
33
|
+
|
|
34
|
+
accept(frame) {
|
|
35
|
+
if (!Number.isSafeInteger(frame?.id) || frame.id < 1) fail()
|
|
36
|
+
if (frame.type === 'response') { this.drop(frame.id); return frame }
|
|
37
|
+
if (frame.type === 'response-start') {
|
|
38
|
+
if (this.#parts.has(frame.id) || this.#parts.size >= 16 || !Number.isSafeInteger(frame.bytes)
|
|
39
|
+
|| frame.bytes <= CHUNK_BYTES || frame.bytes > MAX_RESPONSE) fail()
|
|
40
|
+
this.#parts.set(frame.id, { expected: frame.bytes, size: 0, chunks: [] })
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
const state = this.#parts.get(frame.id)
|
|
44
|
+
if (!state) fail()
|
|
45
|
+
if (frame.type === 'response-chunk') {
|
|
46
|
+
if (frame.index !== state.chunks.length || typeof frame.data !== 'string' || frame.data.length > Math.ceil(CHUNK_BYTES / 3) * 4) fail()
|
|
47
|
+
const bytes = Buffer.from(frame.data, 'base64')
|
|
48
|
+
if (!bytes.length || bytes.length > CHUNK_BYTES || bytes.toString('base64') !== frame.data
|
|
49
|
+
|| state.size + bytes.length > state.expected || this.#buffered + bytes.length > MAX_BUFFERED) fail()
|
|
50
|
+
state.chunks.push(bytes); state.size += bytes.length; this.#buffered += bytes.length
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
if (frame.type !== 'response-end' || state.size !== state.expected) fail()
|
|
54
|
+
const bytes = Buffer.concat(state.chunks)
|
|
55
|
+
this.drop(frame.id)
|
|
56
|
+
if (frame.sha256 !== sha(bytes)) fail()
|
|
57
|
+
const value = JSON.parse(bytes.toString('utf8'))
|
|
58
|
+
if (value.type !== 'response' || value.id !== frame.id) fail()
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Only a trusted handler can render MCP text or pictures. */
|
|
64
|
+
export function normalizeBridgeMcpResult(value) {
|
|
65
|
+
if (!value || !Array.isArray(value.content) || value.content.length > 32
|
|
66
|
+
|| Object.keys(value).some(key => !['content', 'isError'].includes(key))
|
|
67
|
+
|| value.isError !== undefined && typeof value.isError !== 'boolean') fail()
|
|
68
|
+
for (const block of value.content) {
|
|
69
|
+
if (block?.type === 'text') {
|
|
70
|
+
if (Object.keys(block).some(key => !['type', 'text'].includes(key)) || typeof block.text !== 'string') fail()
|
|
71
|
+
} else if (block?.type === 'image') {
|
|
72
|
+
if (Object.keys(block).some(key => !['type', 'data', 'mimeType'].includes(key))
|
|
73
|
+
|| !['image/png', 'image/jpeg', 'image/webp'].includes(block.mimeType) || typeof block.data !== 'string'
|
|
74
|
+
|| block.data.length > 8 * 1024 * 1024) fail()
|
|
75
|
+
const bytes = Buffer.from(block.data, 'base64')
|
|
76
|
+
if (!bytes.length || bytes.toString('base64') !== block.data || bytes.length > 6 * 1024 * 1024) fail()
|
|
77
|
+
} else fail()
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
@@ -5,13 +5,16 @@ import { Server } from '@modelcontextprotocol/server'
|
|
|
5
5
|
import { serveStdio, StdioServerTransport } from '@modelcontextprotocol/server/stdio'
|
|
6
6
|
import { nativeProcess, privateEnvironment } from '../../cli-runtime/src/native-process.mjs'
|
|
7
7
|
import { dirname, isAbsolute } from 'node:path'
|
|
8
|
+
import { BridgeResponseReader } from './cli-bridge-response.mjs'
|
|
8
9
|
|
|
9
10
|
let relay, handle
|
|
10
11
|
const pending = new Map()
|
|
12
|
+
const responses = new BridgeResponseReader()
|
|
11
13
|
let sequence = 0
|
|
12
14
|
const finish = () => {
|
|
13
15
|
for (const value of pending.values()) value.reject(new Error('BROKER_CLOSED'))
|
|
14
16
|
pending.clear()
|
|
17
|
+
responses.clear()
|
|
15
18
|
relay?.end()
|
|
16
19
|
}
|
|
17
20
|
try {
|
|
@@ -35,27 +38,37 @@ try {
|
|
|
35
38
|
const ask = (op, payload, signal) => new Promise((resolve, reject) => {
|
|
36
39
|
if (relay.closed || pending.size >= 16) { reject(new Error('BROKER_UNAVAILABLE')); return }
|
|
37
40
|
const id = ++sequence
|
|
38
|
-
const abort = () => { const value = pending.get(id); pending.delete(id); void relay.send({ type: 'cancel', id }).catch(() => {}); value?.reject(new Error('REQUEST_CANCELLED')) }
|
|
41
|
+
const abort = () => { const value = pending.get(id); pending.delete(id); responses.drop(id); void relay.send({ type: 'cancel', id }).catch(() => {}); value?.reject(new Error('REQUEST_CANCELLED')) }
|
|
39
42
|
if (signal?.aborted) { reject(new Error('REQUEST_CANCELLED')); return }
|
|
40
43
|
const timer = setTimeout(abort, Math.min(remaining, 30_000))
|
|
41
44
|
signal?.addEventListener('abort', abort, { once: true })
|
|
42
45
|
const settle = fn => value => { clearTimeout(timer); signal?.removeEventListener('abort', abort); fn(value) }
|
|
43
46
|
pending.set(id, { resolve: settle(resolve), reject: settle(reject) })
|
|
44
|
-
void relay.send({ type: 'request', id, op, ...payload }).catch(() => { const value = pending.get(id); pending.delete(id); value?.reject(new Error('BROKER_CLOSED')) })
|
|
47
|
+
void relay.send({ type: 'request', id, op, ...payload }).catch(() => { const value = pending.get(id); pending.delete(id); responses.drop(id); value?.reject(new Error('BROKER_CLOSED')) })
|
|
45
48
|
})
|
|
46
49
|
relay.events.on('message', message => {
|
|
47
|
-
if (message?.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (!Number.isSafeInteger(message?.id) || !pending.has(message.id)) return
|
|
51
|
+
try {
|
|
52
|
+
const response = responses.accept(message)
|
|
53
|
+
if (!response) return
|
|
54
|
+
const value = pending.get(response.id); pending.delete(response.id)
|
|
55
|
+
if (response.ok) value?.resolve(response)
|
|
56
|
+
else value?.reject(new Error(response.code ?? 'BROKER_REFUSED'))
|
|
57
|
+
} catch {
|
|
58
|
+
const value = pending.get(message.id); pending.delete(message.id); responses.drop(message.id)
|
|
59
|
+
value?.reject(new Error('INVALID_BRIDGE_RESPONSE'))
|
|
60
|
+
}
|
|
51
61
|
})
|
|
52
62
|
relay.events.on('failure', () => { finish(); void handle?.close() })
|
|
53
63
|
relay.events.once('closed', () => { finish(); void handle?.close() })
|
|
54
64
|
handle = serveStdio(() => {
|
|
55
65
|
const server = new Server({ name: 'dsh-task-capability-bridge', version: '0.0.0' }, { capabilities: { tools: {} } })
|
|
56
|
-
server.setRequestHandler('tools/list', async (_request, ctx) => ({ tools: await ask('list', {}, ctx.mcpReq.signal) }))
|
|
66
|
+
server.setRequestHandler('tools/list', async (_request, ctx) => ({ tools: (await ask('list', {}, ctx.mcpReq.signal)).value }))
|
|
57
67
|
server.setRequestHandler('tools/call', async (request, ctx) => {
|
|
58
|
-
try {
|
|
68
|
+
try {
|
|
69
|
+
const response = await ask('call', { name: request.params.name, args: request.params.arguments ?? {} }, ctx.mcpReq.signal)
|
|
70
|
+
return response.mcpResult ?? { content: [{ type: 'text', text: JSON.stringify(response.value) }] }
|
|
71
|
+
}
|
|
59
72
|
catch (error) { return { isError: true, content: [{ type: 'text', text: error.message }] } }
|
|
60
73
|
})
|
|
61
74
|
server.onclose = finish
|
|
@@ -54,6 +54,9 @@ export async function createCodexSessionCore({ authority, subject, binding, oper
|
|
|
54
54
|
await protectProfile(probe.helper, home)
|
|
55
55
|
const entry = await pinCodexExecutable(suppliedEntry, home)
|
|
56
56
|
const bridge = await createCliBootstrap({ authority, subject, binding, tools, handlers, home: join(home, 'b'), ttlMs: deadlineMs, helper: probe.helper })
|
|
57
|
+
// Codex filters inherited MCP environment variables. Preserve the Host-owned
|
|
58
|
+
// compiler root explicitly while keeping the CLI's private HOME and TEMP.
|
|
59
|
+
const compilerRoot = privateEnvironment(home).DSH_NATIVE_COMPILER_ROOT
|
|
57
60
|
const config = [
|
|
58
61
|
`model = ${JSON.stringify(requestedModel)}`, 'model_provider = "openai"', 'approval_policy = "never"', 'sandbox_mode = "read-only"',
|
|
59
62
|
'web_search = "disabled"', 'cli_auth_credentials_store = "file"', 'suppress_unstable_features_warning = true',
|
|
@@ -65,6 +68,7 @@ export async function createCodexSessionCore({ authority, subject, binding, oper
|
|
|
65
68
|
`enabled_tools = ${JSON.stringify(tools)}`, 'env_vars = []', 'startup_timeout_sec = 20',
|
|
66
69
|
'[mcp_servers.dsh_task.env]', `HOME = ${JSON.stringify(home)}`, `USERPROFILE = ${JSON.stringify(home)}`,
|
|
67
70
|
`TEMP = ${JSON.stringify(join(home, 'tmp'))}`, `TMP = ${JSON.stringify(join(home, 'tmp'))}`,
|
|
71
|
+
...(compilerRoot ? [`DSH_NATIVE_COMPILER_ROOT = ${JSON.stringify(compilerRoot)}`] : []),
|
|
68
72
|
].join('\n') + '\n'
|
|
69
73
|
await writeFile(join(home, 'config.toml'), config, { flag: 'wx', mode: 0o600 })
|
|
70
74
|
if (provisionCredentials) await provisionCredentials(home)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const FRAME = 262_144
|
|
4
|
+
const RAW_FRAME = 18 * 1024 * 1024
|
|
5
|
+
const IMAGE = 6 * 1024 * 1024
|
|
6
|
+
const fail = () => { throw new Error('CLI_FRAME_LIMIT') }
|
|
7
|
+
|
|
8
|
+
// Claude's verbose user/tool_result events can echo the image already delivered over MCP.
|
|
9
|
+
// Keep bounded receipts for those echoes; the model has received the original pixels itself.
|
|
10
|
+
function receipt(frame, rawBytes) {
|
|
11
|
+
const results = frame?.type === 'user' && frame.message?.content
|
|
12
|
+
const toolResult = Array.isArray(results) && results.length > 0 && results.every(block => block?.type === 'tool_result' && typeof block.tool_use_id === 'string')
|
|
13
|
+
if (!toolResult) { if (rawBytes > FRAME) fail(); return frame }
|
|
14
|
+
let imageBytes = 0, images = 0, nodes = 0
|
|
15
|
+
const visit = (value, depth = 0) => {
|
|
16
|
+
if (++nodes > 30000 || depth > 24) fail()
|
|
17
|
+
if (!value || typeof value !== 'object') return value
|
|
18
|
+
if (value.type === 'image') {
|
|
19
|
+
const source = value.source
|
|
20
|
+
const data = source?.type === 'base64' ? source.data : value.data
|
|
21
|
+
const mediaType = source?.type === 'base64' ? source.media_type : value.mimeType
|
|
22
|
+
if (typeof data !== 'string' || data.length > IMAGE / 3 * 4 || !['image/png', 'image/jpeg', 'image/webp'].includes(mediaType)) fail()
|
|
23
|
+
const bytes = Buffer.from(data, 'base64')
|
|
24
|
+
if (!bytes.length || bytes.length > IMAGE || bytes.toString('base64') !== data || ++images > 32 || (imageBytes += bytes.length) > IMAGE * 2) fail()
|
|
25
|
+
return { type: 'image', mediaType, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) return value.map(item => visit(item, depth + 1))
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(item, depth + 1)]))
|
|
29
|
+
}
|
|
30
|
+
const result = visit(frame)
|
|
31
|
+
if (rawBytes > FRAME && !images || Buffer.byteLength(JSON.stringify(result)) > FRAME) fail()
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Bound raw JSON lines separately from the non-image session output budget. */
|
|
36
|
+
export class NativeCliOutput {
|
|
37
|
+
#buffer = ''
|
|
38
|
+
#bytes = 0
|
|
39
|
+
push(text) {
|
|
40
|
+
this.#buffer += text
|
|
41
|
+
const frames = []
|
|
42
|
+
let end
|
|
43
|
+
while ((end = this.#buffer.indexOf('\n')) >= 0) {
|
|
44
|
+
const line = this.#buffer.slice(0, end)
|
|
45
|
+
this.#buffer = this.#buffer.slice(end + 1)
|
|
46
|
+
const rawBytes = Buffer.byteLength(line)
|
|
47
|
+
if (rawBytes > RAW_FRAME) fail()
|
|
48
|
+
if (!line.trim()) continue
|
|
49
|
+
const frame = receipt(JSON.parse(line), rawBytes)
|
|
50
|
+
this.#bytes += Buffer.byteLength(JSON.stringify(frame))
|
|
51
|
+
if (this.#bytes > 8 * 1024 * 1024) fail()
|
|
52
|
+
frames.push(frame)
|
|
53
|
+
}
|
|
54
|
+
if (Buffer.byteLength(this.#buffer) > RAW_FRAME) fail()
|
|
55
|
+
return frames
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -2,6 +2,46 @@ param([ValidateSet('server','client','supervise','probe','protect')][string]$Mod
|
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false, $true)
|
|
4
4
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
5
|
+
$compilerPhase = $true
|
|
6
|
+
try {
|
|
7
|
+
# Host-allocated compilation storage is separate from the CLI's private
|
|
8
|
+
# profile/TEMP. The root hint is not a peer credential or an execution grant.
|
|
9
|
+
$compilerRoot = $env:DSH_NATIVE_COMPILER_ROOT
|
|
10
|
+
$compilerScratch = $env:DSH_NATIVE_COMPILER_SCRATCH
|
|
11
|
+
if ([string]::IsNullOrEmpty($compilerRoot) -or [string]::IsNullOrEmpty($compilerScratch) -or
|
|
12
|
+
$compilerRoot.Length -gt 140 -or $compilerScratch.Length -gt 160 -or
|
|
13
|
+
-not [System.IO.Path]::IsPathRooted($compilerRoot) -or -not [System.IO.Path]::IsPathRooted($compilerScratch) -or
|
|
14
|
+
[System.IO.Path]::GetFileName($compilerRoot) -notmatch '^dshc-[a-f0-9]{16}$' -or
|
|
15
|
+
[System.IO.Path]::GetFileName($compilerScratch) -notmatch '^c-[A-Za-z0-9]{6}$' -or
|
|
16
|
+
-not [string]::Equals([System.IO.Path]::GetDirectoryName($compilerScratch), $compilerRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'COMPILER_STORAGE_INVALID' }
|
|
17
|
+
$ownerSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
18
|
+
foreach ($compilerPath in @($compilerRoot, $compilerScratch)) {
|
|
19
|
+
$directory = [System.IO.DirectoryInfo]::new($compilerPath)
|
|
20
|
+
if (-not $directory.Exists -or ($directory.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'COMPILER_STORAGE_INVALID' }
|
|
21
|
+
$security = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
22
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
23
|
+
$security.SetOwner($ownerSid)
|
|
24
|
+
$security.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($ownerSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
|
25
|
+
$directory.SetAccessControl($security)
|
|
26
|
+
$actual = $directory.GetAccessControl()
|
|
27
|
+
$rules = @($actual.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
28
|
+
if (-not $actual.AreAccessRulesProtected -or $rules.Count -ne 1 -or $rules[0].IdentityReference.Value -ne $ownerSid.Value -or
|
|
29
|
+
$rules[0].AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
30
|
+
$rules[0].FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::FullControl) { throw 'COMPILER_ACL_UNCONFIRMED' }
|
|
31
|
+
}
|
|
32
|
+
if ([System.IO.File]::ReadAllText([System.IO.Path]::Combine($compilerRoot, '.dsh-root')) -ne ('dsh-native-compiler-v1:' + [System.IO.Path]::GetFileName($compilerRoot) + "`n") -or
|
|
33
|
+
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($compilerScratch, '.dsh-owner')) -notmatch '^[a-f0-9]{32}$') { throw 'COMPILER_STORAGE_INVALID' }
|
|
34
|
+
$compilerParameters = [System.CodeDom.Compiler.CompilerParameters]::new()
|
|
35
|
+
$compilerParameters.GenerateInMemory = $true
|
|
36
|
+
$compilerParameters.GenerateExecutable = $false
|
|
37
|
+
$compilerParameters.TempFiles = [System.CodeDom.Compiler.TempFileCollection]::new($compilerScratch, $false)
|
|
38
|
+
[void]$compilerParameters.ReferencedAssemblies.Add('System.dll')
|
|
39
|
+
[void]$compilerParameters.ReferencedAssemblies.Add('System.Core.dll')
|
|
40
|
+
$privateTemps = @{ TEMP = $env:TEMP; TMP = $env:TMP; TMPDIR = $env:TMPDIR }
|
|
41
|
+
try {
|
|
42
|
+
# csc's Win32 resource generator also uses the process TEMP; change it only
|
|
43
|
+
# for compilation, then restore it before admitting any CLI process.
|
|
44
|
+
$env:TEMP = $compilerScratch; $env:TMP = $compilerScratch; $env:TMPDIR = $compilerScratch
|
|
5
45
|
# Only packaged source is compiled. No supplied command or script is evaluated.
|
|
6
46
|
Add-Type -TypeDefinition @'
|
|
7
47
|
using System;
|
|
@@ -286,8 +326,12 @@ public static class DshNativePeer {
|
|
|
286
326
|
return result;
|
|
287
327
|
}
|
|
288
328
|
}
|
|
289
|
-
'@
|
|
290
|
-
|
|
329
|
+
'@ -CompilerParameters $compilerParameters
|
|
330
|
+
} finally {
|
|
331
|
+
foreach ($key in @('TEMP','TMP','TMPDIR')) { [System.Environment]::SetEnvironmentVariable($key, $privateTemps[$key], 'Process') }
|
|
332
|
+
[System.Environment]::SetEnvironmentVariable('DSH_NATIVE_COMPILER_SCRATCH', $null, 'Process')
|
|
333
|
+
}
|
|
334
|
+
$compilerPhase = $false
|
|
291
335
|
if ($Mode -eq 'probe') {
|
|
292
336
|
$result = [DshNativePeer]::ProbeJob()
|
|
293
337
|
$response = @{ peer = 'windows-kernel-pid'; supervisor = 'windows-job'; available = ($result -eq 'OK') }
|
|
@@ -308,4 +352,8 @@ try {
|
|
|
308
352
|
elseif ($Mode -eq 'server') { [DshNativePeer]::Server($spec.endpoint,[int]$spec.rootPid,[string]$spec.rootStart,[int]$spec.ttlMs) }
|
|
309
353
|
elseif ($Mode -eq 'client') { [DshNativePeer]::Client($spec.endpoint,[int]$spec.serverPid,[string]$spec.serverStart,[int]$spec.ttlMs) }
|
|
310
354
|
else { [DshNativePeer]::Supervise($spec.executable,[string[]]$spec.args,$spec.cwd,[int]$spec.ttlMs) }
|
|
311
|
-
} catch {
|
|
355
|
+
} catch {
|
|
356
|
+
$code = if ($compilerPhase) { 'NATIVE_COMPILER_FAILED' } else { $_.Exception.GetBaseException().Message }
|
|
357
|
+
if ($code -notmatch '^[A-Z][A-Z0-9_]{0,79}$') { $code = 'NATIVE_PEER_FAILED' }
|
|
358
|
+
[Console]::Error.WriteLine('NATIVE_PEER_REFUSED: ' + $code); exit 2
|
|
359
|
+
}
|