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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) 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 +18 -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/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,329 @@
1
+ // Bounded JSON-RPC transport over a stdio-shaped pipe, and the pipe itself.
2
+ //
3
+ // Ported and re-grounded from OpenMausBot server/drivers/acp/host.ts at pin
4
+ // 1bc4749aa8307d7d6998bf86de4ef6478460dd93 (Apache-2.0); see licenses/NOTICE.md.
5
+ // The upstream host frames a child's stdout, maps request ids to pending
6
+ // promises, and fails every pending entry when the process ends. This module
7
+ // keeps that shape and makes every bound explicit: pending count, write
8
+ // backlog, per-request deadline and idle deadline are hard limits, and a
9
+ // duplicate, unknown or late response is counted rather than replayed.
10
+
11
+ import { EventEmitter } from 'node:events'
12
+
13
+ import {
14
+ DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_MAX_BACKLOG_BYTES, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING,
15
+ DEFAULT_REQUEST_TIMEOUT_MS, MAX_LOG_PREVIEW, classifyMessage, createFrameDecoder, errorFromWire,
16
+ isValidMessageId, parseFrame, previewMessage,
17
+ } from './rpc-codec.mjs'
18
+
19
+ const SETTLED_ID_MEMORY = 4096
20
+
21
+ /**
22
+ * A child process (or the native supervisor's stdio relay) as one pipe.
23
+ * `exit` resolves with `{ code, signal }` when the peer is really gone.
24
+ */
25
+ export function createChildPipe({ stdin, stdout, exit, maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, maxBacklogBytes = DEFAULT_MAX_BACKLOG_BYTES, log } = {}) {
26
+ if (!stdin || typeof stdin.write !== 'function') throw new TypeError('ACP_PIPE_STDIN_REQUIRED')
27
+ if (!stdout || typeof stdout.on !== 'function') throw new TypeError('ACP_PIPE_STDOUT_REQUIRED')
28
+ const events = new EventEmitter()
29
+ let closed = false
30
+ let queued = 0
31
+ let writes = 0, dropped = 0, writeErrors = 0
32
+ const decoder = createFrameDecoder({
33
+ maxFrameBytes,
34
+ onFrame: line => {
35
+ const message = parseFrame(line)
36
+ if (!message) {
37
+ events.emit('malformed', { reason: 'unparseable', preview: line.slice(0, MAX_LOG_PREVIEW) })
38
+ return
39
+ }
40
+ const classified = classifyMessage(message)
41
+ if (classified.kind === 'invalid') {
42
+ events.emit('malformed', { reason: classified.reason, preview: previewMessage(message) })
43
+ return
44
+ }
45
+ events.emit('message', classified)
46
+ },
47
+ onError: error => {
48
+ log?.({ event: 'acp.frame-limit', message: error.message })
49
+ events.emit('malformed', { reason: error.message, preview: '' })
50
+ },
51
+ })
52
+ stdout.on('data', chunk => decoder.write(chunk))
53
+ stdout.on('end', () => decoder.end())
54
+ stdout.on('error', error => { writeErrors++; events.emit('failure', error) })
55
+ stdin.on('error', () => { writeErrors++ })
56
+ const finish = (code = null, signal = null) => {
57
+ if (closed) return
58
+ closed = true
59
+ events.emit('closed', { code, signal })
60
+ }
61
+ if (typeof exit?.then === 'function') exit.then(value => finish(value?.code ?? null, value?.signal ?? null), () => finish(null, null))
62
+ const write = value => new Promise((resolve, reject) => {
63
+ const wire = `${JSON.stringify(value)}\n`
64
+ const bytes = Buffer.byteLength(wire)
65
+ // The frame bound is a local validation: an oversized value is refused
66
+ // whether or not the peer is still connected.
67
+ if (bytes > maxFrameBytes) { dropped++; reject(Object.assign(new Error('ACP_FRAME_LIMIT'), { acpFrameLimit: true })); return }
68
+ if (closed) { reject(Object.assign(new Error('ACP_PIPE_CLOSED'), { acpPipeClosed: true })); return }
69
+ queued += bytes
70
+ if (queued > maxBacklogBytes) {
71
+ queued -= bytes
72
+ dropped++
73
+ const error = Object.assign(new Error('ACP_BACKLOG_LIMIT'), { acpBacklogLimit: true })
74
+ events.emit('failure', error)
75
+ finish(null, null)
76
+ reject(error)
77
+ return
78
+ }
79
+ writes++
80
+ stdin.write(wire, error => {
81
+ queued -= bytes
82
+ if (error) { writeErrors++; reject(error); return }
83
+ resolve()
84
+ })
85
+ })
86
+ /** Fire-and-forget send: a refusal is counted, never thrown into protocol code. */
87
+ const send = value => { void write(value).catch(() => {}) }
88
+ return Object.freeze({
89
+ events,
90
+ write,
91
+ send,
92
+ end: () => { try { stdin.end() } catch {} },
93
+ get closed() { return closed },
94
+ get stats() { return Object.freeze({ ...decoder.stats, writes, dropped, writeErrors, queuedBytes: queued }) },
95
+ })
96
+ }
97
+
98
+ /** Peer-exit and abort errors carry the evidence a caller needs to settle honestly. */
99
+ const peerExitError = (code, signal) => Object.assign(
100
+ new Error(`ACP agent process exited (code ${code ?? 'null'}, signal ${signal ?? 'null'})`),
101
+ { acpPeerExit: true, exitCode: code, exitSignal: signal },
102
+ )
103
+
104
+ /**
105
+ * The RPC layer: id-mapped requests, notifications and responses, with pending,
106
+ * deadline, idle and abort limits. Inbound requests and notifications are
107
+ * emitted for the session layer to answer; this layer never decides policy.
108
+ */
109
+ export function createStdioRpc({ pipe, maxPending = DEFAULT_MAX_PENDING, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, signal, route, log } = {}) {
110
+ if (!pipe || typeof pipe.write !== 'function' || !pipe.events) throw new TypeError('ACP_RPC_PIPE_REQUIRED')
111
+ if (!Number.isSafeInteger(maxPending) || maxPending < 1) throw new TypeError('ACP_PENDING_LIMIT_INVALID')
112
+ const pending = new Map()
113
+ const settled = new Set()
114
+ let nextId = 1
115
+ let closed = false
116
+ let lastInboundAt = null
117
+ const stats = { requests: 0, responses: 0, notifications: 0, inboundRequests: 0, inboundNotifications: 0, unknownResponses: 0, duplicateResponses: 0, lateResponses: 0, malformed: 0, timeouts: 0, idleTimeouts: 0, aborted: 0, pendingLimitRefusals: 0, peerExits: 0 }
118
+
119
+ const remember = id => {
120
+ settled.add(id)
121
+ if (settled.size > SETTLED_ID_MEMORY) settled.delete(settled.values().next().value)
122
+ }
123
+
124
+ const clearTimers = entry => {
125
+ if (entry.timer) clearTimeout(entry.timer)
126
+ if (entry.idleTimer) clearTimeout(entry.idleTimer)
127
+ }
128
+
129
+ const settleEntry = (entry, error) => {
130
+ if (!pending.delete(entry.id)) return false
131
+ clearTimers(entry)
132
+ remember(entry.id)
133
+ entry.reject(error)
134
+ return true
135
+ }
136
+
137
+ const touch = (owner = null) => {
138
+ lastInboundAt = Date.now()
139
+ for (const entry of pending.values()) {
140
+ // A session's own traffic restarts only its own deadlines; a process-level
141
+ // request (issued before any session exists) restarts on any traffic.
142
+ if (entry.owner === null || entry.owner === owner) entry.armIdle()
143
+ }
144
+ }
145
+
146
+ const failAll = (error, cause) => {
147
+ for (const entry of [...pending.values()]) {
148
+ if (!pending.delete(entry.id)) continue
149
+ clearTimers(entry)
150
+ remember(entry.id)
151
+ entry.reject(error)
152
+ }
153
+ if (cause === 'peer-exit') stats.peerExits++
154
+ }
155
+
156
+ /** Reject every pending request owned by one session; other sessions go on. */
157
+ const failOwner = (owner, error) => {
158
+ if (typeof owner !== 'string' || !owner) return 0
159
+ let failed = 0
160
+ for (const entry of [...pending.values()]) {
161
+ if (entry.owner !== owner) continue
162
+ if (!pending.delete(entry.id)) continue
163
+ clearTimers(entry)
164
+ remember(entry.id)
165
+ entry.reject(error)
166
+ failed++
167
+ }
168
+ return failed
169
+ }
170
+
171
+ const close = (reason = 'closed') => {
172
+ if (closed) return
173
+ closed = true
174
+ failAll(Object.assign(new Error(`ACP connection ${reason}`), { acpClosed: true }), 'close')
175
+ pipe.events.off('message', onMessage)
176
+ pipe.events.off('malformed', onMalformed)
177
+ pipe.events.off('failure', onFailure)
178
+ pipe.events.off('closed', onClosed)
179
+ pipe.end()
180
+ }
181
+
182
+ function onMessage(classified) {
183
+ touch(typeof route === 'function' ? route(classified) ?? null : null)
184
+ if (classified.kind === 'response') {
185
+ stats.responses++
186
+ const entry = pending.get(classified.id)
187
+ if (!entry) {
188
+ // A response for an id this side never issued, or already settled.
189
+ if (settled.has(classified.id)) { stats.duplicateResponses++; stats.lateResponses++ } else stats.unknownResponses++
190
+ return
191
+ }
192
+ pending.delete(classified.id)
193
+ clearTimers(entry)
194
+ remember(classified.id)
195
+ if (classified.error !== undefined) {
196
+ entry.reject(errorFromWire(classified.error, entry.method))
197
+ return
198
+ }
199
+ entry.onReceive?.(classified.result)
200
+ entry.resolve(classified.result)
201
+ return
202
+ }
203
+ if (classified.kind === 'request') {
204
+ stats.inboundRequests++
205
+ pipe.events.emit('rpc-request', classified)
206
+ return
207
+ }
208
+ stats.inboundNotifications++
209
+ pipe.events.emit('rpc-notification', classified)
210
+ }
211
+
212
+ function onMalformed(detail) {
213
+ stats.malformed++
214
+ log?.({ event: 'acp.malformed', reason: detail?.reason ?? 'unknown' })
215
+ }
216
+
217
+ function onFailure(error) {
218
+ log?.({ event: 'acp.pipe-failure', message: error?.message ?? String(error) })
219
+ failAll(error instanceof Error ? error : new Error(String(error)), 'failure')
220
+ }
221
+
222
+ function onClosed(detail) {
223
+ if (closed) return
224
+ closed = true
225
+ failAll(peerExitError(detail?.code ?? null, detail?.signal ?? null), 'peer-exit')
226
+ pipe.events.off('message', onMessage)
227
+ pipe.events.off('malformed', onMalformed)
228
+ pipe.events.off('failure', onFailure)
229
+ pipe.events.off('closed', onClosed)
230
+ }
231
+
232
+ pipe.events.on('message', onMessage)
233
+ pipe.events.on('malformed', onMalformed)
234
+ pipe.events.on('failure', onFailure)
235
+ pipe.events.on('closed', onClosed)
236
+
237
+ const onAbort = () => close('aborted')
238
+ signal?.addEventListener('abort', onAbort, { once: true })
239
+ if (signal?.aborted) onAbort()
240
+
241
+ return Object.freeze({
242
+ /** Peer-exit and abort errors carry the evidence a caller needs to settle honestly. */
243
+ /** The inbound event bus: `rpc-request`, `rpc-notification`, `malformed`, `failure`, `closed`. */
244
+ events: pipe.events,
245
+ request(method, params, options = {}) {
246
+ if (closed) return Promise.reject(Object.assign(new Error('ACP connection closed'), { acpClosed: true }))
247
+ if (typeof method !== 'string' || !method) return Promise.reject(Object.assign(new TypeError('ACP_METHOD_REQUIRED'), { acpInvalidRequest: true }))
248
+ if (pending.size >= maxPending) {
249
+ stats.pendingLimitRefusals++
250
+ return Promise.reject(Object.assign(new Error(`ACP pending limit reached (${maxPending})`), { acpPendingLimit: true }))
251
+ }
252
+ if (options.signal?.aborted) return Promise.reject(Object.assign(new Error('ACP request aborted'), { acpAborted: true }))
253
+ const timeoutMs = Number.isSafeInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : requestTimeoutMs
254
+ const idleMs = Number.isSafeInteger(options.idleMs) && options.idleMs > 0 ? options.idleMs : idleTimeoutMs
255
+ const id = nextId++
256
+ return new Promise((resolve, reject) => {
257
+ const entry = {
258
+ id, method, resolve, reject,
259
+ owner: typeof options.owner === 'string' && options.owner ? options.owner : null,
260
+ onReceive: typeof options.onReceive === 'function' ? options.onReceive : null,
261
+ timer: null, idleTimer: null, idleMs,
262
+ armIdle: () => {
263
+ if (!(entry.idleMs > 0)) return
264
+ if (entry.idleTimer) clearTimeout(entry.idleTimer)
265
+ entry.idleTimer = setTimeout(() => {
266
+ if (!pending.delete(id)) return
267
+ remember(id)
268
+ stats.idleTimeouts++
269
+ reject(Object.assign(new Error(`${method} stopped responding`), { acpIdleTimeout: true }))
270
+ }, entry.idleMs)
271
+ entry.idleTimer.unref?.()
272
+ },
273
+ }
274
+ entry.timer = setTimeout(() => {
275
+ if (!pending.delete(id)) return
276
+ remember(id)
277
+ stats.timeouts++
278
+ reject(Object.assign(new Error(`${method} timed out after ${timeoutMs}ms`), { acpRequestTimeout: true }))
279
+ }, timeoutMs)
280
+ entry.timer.unref?.()
281
+ entry.armIdle()
282
+ pending.set(id, entry)
283
+ stats.requests++
284
+ const onRequestAbort = () => {
285
+ if (!pending.delete(id)) return
286
+ clearTimers(entry)
287
+ remember(id)
288
+ stats.aborted++
289
+ reject(Object.assign(new Error(`${method} aborted`), { acpAborted: true }))
290
+ }
291
+ if (options.signal) {
292
+ entry.onAbort = onRequestAbort
293
+ options.signal.addEventListener('abort', onRequestAbort, { once: true })
294
+ }
295
+ pipe.write({ jsonrpc: '2.0', id, method, params: params === undefined ? {} : params }).catch(error => {
296
+ if (!pending.delete(id)) return
297
+ clearTimers(entry)
298
+ remember(id)
299
+ if (options.signal) options.signal.removeEventListener?.('abort', onRequestAbort)
300
+ reject(error)
301
+ })
302
+ })
303
+ },
304
+ /** A notification: no id, no reply expected. */
305
+ notify(method, params) {
306
+ if (closed || typeof method !== 'string' || !method) return false
307
+ stats.notifications++
308
+ pipe.send({ jsonrpc: '2.0', method, params: params === undefined ? {} : params })
309
+ return true
310
+ },
311
+ /** Answer a server request. Unknown ids are the peer's problem, not ours. */
312
+ respond(id, result) {
313
+ if (closed || !isValidMessageId(id)) return false
314
+ pipe.send({ jsonrpc: '2.0', id, result: result === undefined ? {} : result })
315
+ return true
316
+ },
317
+ respondError(id, code, message, data) {
318
+ if (closed || !isValidMessageId(id)) return false
319
+ pipe.send({ jsonrpc: '2.0', id, error: { code, message, ...(data !== undefined ? { data } : {}) } })
320
+ return true
321
+ },
322
+ get pendingCount() { return pending.size },
323
+ failOwner,
324
+ get closed() { return closed },
325
+ get lastInboundAt() { return lastInboundAt },
326
+ get stats() { return Object.freeze({ ...stats, pending: pending.size }) },
327
+ close,
328
+ })
329
+ }
@@ -0,0 +1,281 @@
1
+ # packages/bot-engines
2
+
3
+ Native Bot conversation engines: a closed catalog of the 26 upstream OpenMausBot
4
+ built-in drivers, plus the first ported engine — the **Claude** conversation-session
5
+ adapter, its public LLM bridge and Host-owned registration. The product can explicitly
6
+ configure a pinned Claude text/subscription route and select it through the existing Bot
7
+ model binding. Serving, grants, request history and the one money authority remain in the Host.
8
+
9
+ Status: `claude` is implemented and covered by real offline child-process tests. Every
10
+ other catalog entry is explicitly **unavailable** with a concrete reason and target
11
+ capability. Custom ACP engines remain in migration scope: the generic ACP core now exists
12
+ in `packages/bot-acp` (session lifecycle, permissions, bounded stdio transport, native
13
+ launch — offline-verified, **not** wired into product composition and no ACP CLI claimed
14
+ usable), so `customAcp` is gated on trusted owner registration of pinned entries, grants
15
+ and the permission broker rather than on the core itself.
16
+
17
+ ## Modules
18
+
19
+ | File | Purpose |
20
+ | --- | --- |
21
+ | `src/driver-catalog.mjs` | Closed 26-entry catalog (`BOT_ENGINE_DRIVERS`, `resolveBotEngine`, `assertNoAdHocDriverInput`) |
22
+ | `src/claude-engine.mjs` | `createClaudeEngine` / `pinClaudeEntry` — the conversation-session adapter |
23
+ | `src/claude-protocol.mjs` | Pure stream-json helpers (frame classification, usage/cost math, broker wire) |
24
+ | `src/claude-permission-broker.mjs` | Host-side ask/answer broker (named pipe / unix socket, fail-closed) |
25
+ | `src/claude-permission-proxy.mjs` | MCP stdio `approve` proxy mounted as `mcp__dsh` in the CLI's `--mcp-config` |
26
+ | `src/claude-llm-turn.mjs` | Pure request projection for the LLM route (capability gate, retained-history continuity, usage/finish mapping) |
27
+ | `src/claude-llm-adapter.mjs` | `ClaudeLlmAdapter` / `createClaudeLlmAdapter` — the public `LlmAdapter` bridge over the retained Claude session |
28
+ | `src/host-registrations.mjs` | Closed Host profile validation, public model registration and private CLI credential provisioning |
29
+ | `licenses/NOTICE.md`, `licenses/LICENSE-OpenMausBot` | Upstream attribution and full Apache-2.0 license (pin `1bc4749a…`) |
30
+
31
+ ## Policy invariants
32
+
33
+ - **Closed catalog.** Exactly the 26 upstream built-in driver ids exist. Unknown ids are
34
+ an error, not a fallback. `customAcp` awaits the generic ACP core in `packages/bot-acp`
35
+ (present, offline-verified, unwired) plus trusted owner registration of pinned entries,
36
+ grants and the permission broker. Turn/model request JSON cannot register a command.
37
+ - **Authority is Host state.** Model routes, tools, executable paths, digests,
38
+ environments and credentials never arrive through turn payloads. A driver request
39
+ carrying any of `executable/path/command/endpoint/url/args/env/credentials/apiKey/
40
+ token/autoApprove/alwaysAllow/bypassPermissions` is rejected (`HOST_OWNED_ENGINE_INPUT`).
41
+ - **Fail-closed permissions.** `bypassPermissions` is not an available mode. `--tools ""`
42
+ disables built-in tools, including reads and edits that otherwise need no prompt.
43
+ `--allowedTools` only pre-approves the configured MCP tools. Only `mcp__dsh_task__<grantedTool>`
44
+ permission asks can ever be allowed; everything else (e.g. `Bash`) is auto-denied by the
45
+ engine without reaching the host. Duplicate ask ids, post-close asks and unanswered
46
+ permission asks all deny.
47
+ - **Configuration boundary.** `--setting-sources ""` excludes user/project/local
48
+ settings, `disableAllHooks` disables their hooks, and `--disable-slash-commands`
49
+ disables skills and commands. The private home and strict MCP config are retained.
50
+ Machine-managed CLI policy still applies; this is not a filesystem sandbox.
51
+ Subscription credentials are provisioned into the private home by the Host; bare
52
+ mode is not used because it ignores OAuth login. A CLI lacking required flags must
53
+ fail startup; it must not retry with broader settings.
54
+ - **One ledger.** Cost/usage figures flow only to the Host hooks (`onBeforeRequest`,
55
+ `onObserve`, `onSettle`, `onClose`). Values the CLI does not report stay `null` /
56
+ `'unknown'`; nothing is fabricated here.
57
+ - **Every turn re-checks authorization** (admission hook, model route, grant revision,
58
+ broker liveness), and a refusal before dispatch provably dispatches nothing.
59
+ - **Unknown outcomes are never blindly replayed.** A `--resume` that the CLI refuses
60
+ (error result before init, or exit before any frame) settles `resume-refused` and stops
61
+ — the earlier task is never replayed into a fresh session.
62
+ - **Stop requests are not stop confirmations.** `treeStopped: true` appears only from the
63
+ launcher's own accounting (Windows job object `ActiveProcesses == 0`).
64
+
65
+ ## Turn lifecycle fence
66
+
67
+ - **The turn slot is reserved synchronously.** `submitTurn` takes the single slot before
68
+ its first `await` (the admission hook). A second submit in the same tick — or while
69
+ admission, launch, dispatch, result or the Host settlement is pending — is refused
70
+ `ENGINE_TURN_IN_PROGRESS` with zero extra admissions and zero launches. The slot is
71
+ freed only by a completed Host settlement (or an admission refusal, which provably
72
+ dispatched nothing); a legal next turn may then be submitted.
73
+ - **The Host settlement completes the turn.** The public `submitTurn` promise and the
74
+ `turn` event fire only after the unique `onSettle` callback ran. If the settlement
75
+ hook throws, the turn promise rejects with `ENGINE_SETTLE_HOOK_FAILED`, the native
76
+ evidence attached untouched (`error.turnResult`), `fundingStatus: 'unknown'` (the CLI
77
+ really may have run), an `turn-settle-failed` event is emitted, and the engine stops:
78
+ no next turn, no fresh process pretending success, never a replay.
79
+ - **Post-admission failures settle exactly once**, each receipt bound to its `turnId`
80
+ and the engine's `operationId` (no second ledger): a failure provably before the
81
+ prompt was written to the CLI's stdin settles `not-run` with `usage: null` and
82
+ `costUsd: null` (nothing is invented); a failed stdin write settles `unknown`
83
+ (may-dispatch). A failed write does not kill the engine — the retained process may
84
+ serve the next turn.
85
+ - **Old generations cannot settle new turns.** A `result` frame only settles the turn
86
+ currently in flight on the process that produced it; frames from a dead or former
87
+ process generation are dropped at that state gate (they do not even count as strays),
88
+ and a late twin result after the settle gate is counted (`strayResults`), never applied.
89
+ - **stop() ends the native tree first**, independently of every Host hook: the current
90
+ process and any still-pending launch are ended before waiting on the settlement chain,
91
+ the `tree-stopped` event carries the supervisor's job-object evidence while a slow
92
+ settlement is still pending, and `stop()` itself resolves only after the settlement
93
+ chain and `onClose` ran. A trusted hook that never returns leaves `stop()` pending —
94
+ observable through `snapshot().settlementPending` — but the tree above is already down.
95
+ - **`hooks.onObserve` is observation, not the ledger.** Its failures are counted
96
+ (`snapshot().observeHookFailures`) and emitted (`observe-failed`), never silently
97
+ swallowed, and never a turn or financial outcome.
98
+
99
+ ## The Claude engine in one page
100
+
101
+ ```
102
+ createClaudeEngine({
103
+ authority, subject, binding, operationId, // trusted Host state
104
+ entry: { executable, sha256, version, // digest-pinned native entry
105
+ script?, scriptSha256? }, // (script: pinned cli.js-style entry)
106
+ runtimeRoot, workspace, // isolated dirs
107
+ model: { catalogProvider, catalogModel }, // asserted against the authority
108
+ permissionMode: 'default'|'acceptEdits'|'auto', // bypassPermissions refused
109
+ tools: ['read_notes'], handlers: [...], // committed-grant MCP bridge
110
+ hooks: { onBeforeRequest, onObserve, onSettle, onClose },
111
+ onPermissionRequest?, provisionCredentials?, deadlineMs, signal, launcher?,
112
+ })
113
+ → { submitTurn({text}), respondToRequest(id, {behavior,message?}), stop(), snapshot(), events }
114
+ ```
115
+
116
+ - **Session**: one retained CLI process per session (`-p --output-format stream-json
117
+ --input-format stream-json --verbose --include-partial-messages --strict-mcp-config`).
118
+ The `system/init` frame confirms the session id and the model; a model mismatch is
119
+ fatal. After a process exit the next turn relaunches with `--resume <session_id>`.
120
+ - **Turns**: `submitTurn({ text })` — text (1..64000 bytes, no NUL) is the ONLY accepted
121
+ field. Each turn is settled by the CLI's own `result` frame.
122
+ - **Cost**: `total_cost_usd` is a running total; a turn's cost is the delta since the
123
+ previous result (first reading taken whole; a shrinking total taken whole — the CLI may
124
+ restore an older baseline after `--resume`).
125
+ - **Stop evidence**: `{status, pid, exitCode, nativeStopReason, containment, treeStopped,
126
+ forcedKill, stopRequestedIsNotConfirmation}` — `status: 'confirmed'` only when the
127
+ launcher's job-object accounting says the whole tree ended.
128
+
129
+ ## The Claude LLM bridge (`claude-llm-adapter.mjs`)
130
+
131
+ The retained Claude session as a **public SDK route**: a `LlmAdapter` the Host registers
132
+ with the installed `ctx.llm.registerAdapter([provider], adapter)`, so one Bot conversation
133
+ becomes one `provider/model` route that the Host's own `llm/stream` waterfall — the same one
134
+ an integrated turn goes through — admits, prices and receipts. The bridge holds no money and
135
+ issues no receipt of its own; it is not a second chat surface and not a second ledger.
136
+
137
+ ```
138
+ createClaudeLlmAdapter({
139
+ provider, model, // the one committed route
140
+ funding: 'subscription', // the only open kind
141
+ entry | entryFor, // Host pinned installation reference
142
+ runtimeRoot, workspace | workspaceFor, // isolated absolute dirs
143
+ identityFor: ({provider, model, sessionId}) => ({ // trusted Host resolver
144
+ subject, botId, authority, binding, persona?,
145
+ }),
146
+ hooks, onPermissionRequest, provisionCredentials, // Host state, never request state
147
+ tools, handlers, permissionMode, deadlineMs, launcher, engineFactory,
148
+ })
149
+ → LlmAdapter { listModels, resolveModel, prepareCall, stream, replaceRoute, close, snapshot }
150
+ ```
151
+
152
+ - **Trust boundary.** Only a Host constructs it. A request may carry
153
+ `provider/model/messages/system/signal/sessionId` and nothing else: an extra field
154
+ (identity, path, command, credential, engine configuration) is refused
155
+ (`CLAUDE_ROUTE_UNKNOWN_REQUEST_FIELD`), and so are `tools`, `toolHistory`, `maxTokens`,
156
+ `reasoningEffort`, `temperature`, `stop` and `purpose` — capabilities this route cannot
157
+ honour and therefore does not silently ignore.
158
+ - **One session per conversation.** `stream(options)` is one turn: the trailing new user
159
+ text is projected out of the request history (the retained CLI already holds everything
160
+ before it) and dispatched on the one native session. Continuity is proved by durable
161
+ message id and by the SDK's own model provenance; a rewritten prefix, an unseen history, a
162
+ repeated dispatch, a changed Bot/model/persona/grant revision or a different route
163
+ generation retire the old session and refuse — never a quiet drop and never a replay.
164
+ - **Honest outcomes.** Usage the CLI does not report emits no `usage` chunk; a turn with no
165
+ output, an `unknown` outcome or a cancellation is an `error`/`aborted` finish, never a
166
+ success. Exactly one finish per turn, emitted only after the engine's own Host settlement
167
+ ran. Model provenance in the replay state proves the CLI-configured init only.
168
+ - **Cancellation belongs to the turn.** The session is never bound to the first turn's
169
+ short-lived signal; cancelling stops the native tree, waits for the engine's settlement and
170
+ retires the session. `close()` refuses further calls, waits for an engine that is still
171
+ being created and stops every live tree — it creates nothing.
172
+
173
+ ## Tests (offline, real child processes)
174
+
175
+ ```
176
+ node --test --test-concurrency=1 tests/bot-engine-llm-adapter.test.mjs # 33 pure cases, no process
177
+ node --test --test-concurrency=1 tests/bot-engine-llm-adapter.native.mjs # 9 native cases
178
+ node --test --test-concurrency=1 tests/bot-engine-catalog.test.mjs tests/bot-engine-protocol.test.mjs tests/bot-engine-claude-session.native.mjs
179
+ node scripts/verify-bot-engines.mjs
180
+ ```
181
+
182
+ The native suite installs the real Cordis + `@deepseek-ai/dsh-llm` `LlmRuntime`, registers
183
+ this adapter through the public `registerAdapter`/`prepareCall` seam, and drives the real
184
+ `createClaudeEngine` (no engine-factory seam) through the real native-peer supervisor into a
185
+ real Node child (`tests/fixtures/bot-engine-claude-fixture.mjs`). It proves: two turns on one
186
+ retained PID with only the new input reaching the CLI, a same-tick second turn refused by the
187
+ engine fence, a refused `--resume` that is never a replay, a turn lost to a dead process
188
+ refused as unknown, cancellation and teardown, a route re-registration refused on the
189
+ captured generation, a revoked grant retiring the native tree, an API-funded route refused
190
+ before anything launches, and the parent's real `createModelAccounting` recording exactly one
191
+ receipt with the CLI-reported usage and no invented cost.
192
+
193
+ No claude CLI, no network. The native suite drives a real Node fixture
194
+ (`tests/fixtures/bot-engine-claude-fixture.mjs`) through the real PowerShell
195
+ native-peer `supervise` launcher: two-turn retained session, permission allow/deny/
196
+ question flows, out-of-band host answers, missing usage, utf8/oversize/split/stray
197
+ frames, model mismatch, admission refusal with zero dispatch, mid-turn revoke, host stop
198
+ and abort with job-object tree-stop evidence, and both resume-refusal paths. Two further
199
+ groups prove the turn-lifecycle fence: a native group (same-tick double submit, stray
200
+ results during a pending settlement, a failing settlement hook, tree stop ahead of a
201
+ slow settlement, a failing observation hook) and a launcher-seam group whose gated
202
+ in-memory supervisor exercises the states a healthy process never produces (stuck
203
+ admission, stop during admission/launch, stdin write failure, a throwing `supervise()`,
204
+ a tampered entry script, late frames from a dead generation). Gates are explicit
205
+ promises; the seam models the pipe's causal order (frames cannot overtake the stdin
206
+ write completion). Run serially (`--test-concurrency=1`); the machine has memory limits.
207
+
208
+ ## Remaining ports and acceptance
209
+
210
+ - **ACP products**: the generic core is implemented; trusted owner registration and
211
+ real-driver integration are still required for the upstream ACP drivers.
212
+ - **Other engine ports** (`pending-engine-port`): codex, museCode, pi.
213
+ - **API providers** (8): stateless OpenAI chat-completions channels — they belong to the
214
+ Host model-routing channel, not to conversation engines.
215
+ - **Host acceptance**: configured Claude routes have whole-product offline native tests.
216
+ Live installed CLI/account acceptance, broader controls and remaining engines are pending.
217
+
218
+ ## Configured product route
219
+
220
+ The unreleased Bot plugin accepts a Host-owned `conversationEngines` array. Each profile
221
+ has `id`, `driver: 'claude'`, a unique `provider`, exact `model`, optional `name`, pinned
222
+ `entry` (`executable`, `sha256`, `version`, optional `script`/`scriptSha256`),
223
+ `credentialRef: 'ds-secret/NAME'`, optional `deadlineMs` (default 30 minutes), and
224
+ optional `hostTools` (default false).
225
+ The entry comes from installation verification; this setting neither installs nor logs in.
226
+
227
+ `credentialRef` selects an explicit Host-only secret with an empty Bot scope. Its value
228
+ is the opaque JSON content of the selected CLI credential profile. The Host validates it
229
+ and writes it to the protected engine home under `.claude/.credentials.json`; ambient
230
+ user accounts are not discovered. Changes/removal retire the old Bot session before the
231
+ next request. The Host-confirmed Bot persona uses a private `system-prompt.txt` through
232
+ `--append-system-prompt-file`. Neither credential nor persona contents go in process argv.
233
+
234
+ Enable the independent runtime and choose the configured model in the existing Bot
235
+ settings. The trusted catalog permits subscription funding for this route. It exposes
236
+ text conversation by default. With `hostTools: true`, the native process also
237
+ receives a private MCP server for the Bot's available file, approved container
238
+ command, browser, desktop, operation, artifact, Task and data tools. The catalog is resolved when a native
239
+ session starts. Start a new conversation to refresh it after adding tools. Each
240
+ call rechecks current route funding, credentials, committed Bot grants and paired
241
+ computer scope. File writes and container commands use the existing Task service. A retained child can call
242
+ tools only during its admitted turn. Cancellation reaches the MCP handler and
243
+ requests cancellation of a submitted Task when the Bot has that permission. Its
244
+ durable record retains the actual outcome. Bridge request IDs are single-use
245
+ within a process connection. An uncertain write returns its original Task ID
246
+ and a reconciliation instruction so the model can query `bot_task_status`.
247
+ Browser tools open, snapshot, type, click and close an Agent browser. Desktop tools capture PNG,
248
+ click, type, press keys and scroll. The Host stores conversation-owned operation identities before
249
+ dispatch. Unknown mutations fence later mutations on that target until `bot_remote_operation_status`
250
+ or `bot_remote_operation_cancel` reads the original Agent operation. Browser sessions and screenshot
251
+ references belong to the Bot conversation that created them. Screenshots use the Agent artifact
252
+ channel and arrive as MCP image content, chunked over the private pipe with bounded reassembly.
253
+ Verbose CLI `user/tool_result` image echoes are reduced to size and SHA256 receipts before Host
254
+ events are emitted. The CLI receives the original pixels through MCP. Echo parsing allows bounded
255
+ image frames while retaining the ordinary text frame and session output limits.
256
+ The five data tools share their descriptions and business argument projection with the SDK registry.
257
+ They use the Bot's existing private data engine and controlled library under its committed `tool/data`
258
+ grant, in both chat modes. `data_show.chart` has a validated nested object schema. Saved tables and
259
+ cards are readable in the Data panel, and CSV exports are written to the same Bot library. Current
260
+ support is CSV/TSV/JSON, SQLite, bar/line/area and CSV export. Native cancellation reaches the fixed
261
+ data worker; a cancelled library read cannot later start a table write. Committed changes are not
262
+ rolled back by cancellation, and failed or timed-out data calls are never automatically retried.
263
+ DuckDB and remaining data formats/charts/exports, team tools, browser script/upload/download and rich
264
+ native tool transcripts remain pending.
265
+ Independent chat now supports explicit new conversations. A persisted reset intent blocks
266
+ new turns until the old native tree is proven stopped (or proven never started); unknown
267
+ results stay blocked across restart. Old transcripts remain readable. Whole-product native
268
+ tests prove the next explicit send launches a fresh process with only the new context,
269
+ and resetting itself creates no model receipt. Live account onboarding remains pending. See
270
+ `tests/product-conversation-engine.test.mjs` for the offline whole-product acceptance.
271
+
272
+ The same text route now accepts requests made by the actual installed AgentLoop in
273
+ integration mode. A Host-only projection verifies the SDK marker, frozen request,
274
+ canonical live Session messages and empty tool history, preserves appended Host facts,
275
+ and uses the Host Session persona. Capability assembly omits unsupported native tool
276
+ declarations for that request; Main keeps its full Host tool authority. Switching back
277
+ to an API route restores its tool declarations. The offline public-loop test proves two
278
+ native turns in one child, followed by an explicit API turn with three accounting
279
+ receipts total. The opt-in Host MCP path is tested with the actual SDK, native
280
+ subprocesses, Noise transport, paired Agent and SQLite Task service in both chat modes.
281
+ The remaining native tool families and drivers need further integration.