@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,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 wire = { type: 'response', id, ...value }
55
- void relay.send(Buffer.byteLength(JSON.stringify(wire)) > MAX_BYTES ? { type: 'response', id, ok: false, code: 'OUTPUT_LIMIT' } : wire).catch(() => {})
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 => !['type', 'id', 'op', 'name', 'args'].includes(key))) throw new PolicyError('INVALID_REQUEST')
66
- if (active.has(id) || active.size >= 16) throw new PolicyError('BUSY')
67
- if (message.op === 'list') { reply(id, { ok: true, value: authority.list(lease.token, ownedBinding).map(name => registry.get(name).definition) }); return }
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 controller = new AbortController(); active.set(id, controller)
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
- const value = await authority.run(lease.token, ownedBinding, message.name, ({ signal }) => {
72
- const handler = registry.get(message.name)
73
- return handler.execute(handler.parse(message.args), { signal, binding: ownedBinding })
74
- }, { signal: controller.signal })
75
- reply(id, { ok: true, value })
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?.type !== 'response' || !Number.isSafeInteger(message.id)) return
48
- const value = pending.get(message.id); pending.delete(message.id)
49
- if (message.ok) value?.resolve(message.value)
50
- else value?.reject(new Error(message.code ?? 'BROKER_REFUSED'))
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 { return { content: [{ type: 'text', text: JSON.stringify(await ask('call', { name: request.params.name, args: request.params.arguments ?? {} }, ctx.mcpReq.signal)) }] } }
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
- try {
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 { [Console]::Error.WriteLine('NATIVE_PEER_REFUSED: ' + $_.Exception.GetBaseException().Message); exit 2 }
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
+ }