openmausbot 0.1.85 → 0.1.87

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
  62. package/dist/assets/arc-C88Blc2v.js +1 -0
  63. package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
  64. package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
  65. package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
  66. package/dist/assets/channel-taCNRlUU.js +1 -0
  67. package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
  68. package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
  69. package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
  70. package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
  71. package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
  72. package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
  73. package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
  74. package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
  75. package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
  76. package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
  77. package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
  78. package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
  79. package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
  80. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  81. package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
  82. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  83. package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
  84. package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
  85. package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
  86. package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
  87. package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
  88. package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
  89. package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
  90. package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
  91. package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
  92. package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
  93. package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
  94. package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
  95. package/dist/assets/graph-D8UmR1vK.js +1 -0
  96. package/dist/assets/index-BwdMAQ6p.css +1 -0
  97. package/dist/assets/index-ByJennr7.js +581 -0
  98. package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
  99. package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
  102. package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
  103. package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
  104. package/dist/assets/linear-DBJGPmjT.js +1 -0
  105. package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
  106. package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
  107. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  108. package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
  109. package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
  110. package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
  111. package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
  112. package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
  113. package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
  114. package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
  115. package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
  116. package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
  117. package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
  118. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  119. package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
  120. package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
  121. package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
  122. package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
  123. package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
  124. package/dist/index.html +2 -2
  125. package/dist-server/browser-proxy.js +2 -1
  126. package/dist-server/container-mcp.js +1047 -173
  127. package/dist-server/drivers/agents-proxy.js +16121 -1504
  128. package/dist-server/drivers/phone-proxy.js +33 -2
  129. package/dist-server/enterprise/LICENSE +32 -0
  130. package/dist-server/enterprise/server/index.js +5 -5
  131. package/dist-server/index.js +16134 -7446
  132. package/dist-server/local-computer-proxy.js +3 -3
  133. package/dist-server/local-computer.js +53 -17
  134. package/dist-server/mcp-server.js +3 -1
  135. package/dist-server/openmausbot.js +9353 -6124
  136. package/dist-server/pair-cli.js +9353 -6124
  137. package/dist-server/permission-proxy.js +29 -9
  138. package/dist-server/server/admin-activity.js +410 -0
  139. package/dist-server/server/attachments.js +55 -0
  140. package/dist-server/server/auto-approve.js +4 -3
  141. package/dist-server/server/auto-vm-claims.js +1 -0
  142. package/dist-server/server/bot-package.js +18 -292
  143. package/dist-server/server/bot-profile.js +5 -17
  144. package/dist-server/server/bot-visibility.js +462 -0
  145. package/dist-server/server/box-create-idempotency.js +3 -3
  146. package/dist-server/server/box.js +59 -56
  147. package/dist-server/server/browser-proxy.js +3 -2
  148. package/dist-server/server/browser-runtime.js +38 -2
  149. package/dist-server/server/channel-queue.js +2 -0
  150. package/dist-server/server/cli-prompts.js +24 -2
  151. package/dist-server/server/cli.js +81 -12
  152. package/dist-server/server/command-allowlist.js +154 -0
  153. package/dist-server/server/composio.js +49 -11
  154. package/dist-server/server/computer-wait.js +22 -9
  155. package/dist-server/server/config.js +133 -17
  156. package/dist-server/server/connector-verdict.js +141 -0
  157. package/dist-server/server/container-computer.js +1 -1
  158. package/dist-server/server/decision-log.js +209 -48
  159. package/dist-server/server/default-model-selection.js +29 -3
  160. package/dist-server/server/delegations.js +97 -44
  161. package/dist-server/server/drivers/acp/core.js +163 -33
  162. package/dist-server/server/drivers/acp/grok.js +4 -3
  163. package/dist-server/server/drivers/agents-call.js +1162 -0
  164. package/dist-server/server/drivers/agents-catalog.js +689 -0
  165. package/dist-server/server/drivers/agents-client.js +28 -0
  166. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  167. package/dist-server/server/drivers/boxagent.js +174 -36
  168. package/dist-server/server/drivers/builtIn.js +2 -0
  169. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  170. package/dist-server/server/drivers/chat-images.js +54 -0
  171. package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
  172. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  173. package/dist-server/server/drivers/claude.js +28 -8
  174. package/dist-server/server/drivers/codex-instructions.js +48 -6
  175. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  176. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  177. package/dist-server/server/drivers/codex.js +121 -34
  178. package/dist-server/server/drivers/grok.js +2 -1
  179. package/dist-server/server/drivers/mistral.js +115 -0
  180. package/dist-server/server/drivers/openai-chat.js +155 -32
  181. package/dist-server/server/drivers/openai-compat.js +16 -8
  182. package/dist-server/server/drivers/permission-command.js +35 -0
  183. package/dist-server/server/drivers/phone-proxy.js +47 -2
  184. package/dist-server/server/drivers/pi.js +75 -4
  185. package/dist-server/server/drivers/prompt-split.js +87 -0
  186. package/dist-server/server/enterprise.js +97 -16
  187. package/dist-server/server/external-runtime.js +83 -0
  188. package/dist-server/server/fleet.js +1 -1
  189. package/dist-server/server/group-thread-usage.js +103 -0
  190. package/dist-server/server/guarded-requests.js +60 -0
  191. package/dist-server/server/harness/http.js +61 -0
  192. package/dist-server/server/harness/registry.js +1 -0
  193. package/dist-server/server/hosted-models.js +151 -0
  194. package/dist-server/server/hosted-slack.js +11 -0
  195. package/dist-server/server/index.js +3091 -658
  196. package/dist-server/server/local-computer.js +72 -23
  197. package/dist-server/server/managed-desktop.js +79 -14
  198. package/dist-server/server/managed-policy.js +153 -0
  199. package/dist-server/server/mcp-bridge.js +6 -6
  200. package/dist-server/server/message-db.js +28 -0
  201. package/dist-server/server/model-context-window.js +7 -0
  202. package/dist-server/server/model-prices.js +121 -0
  203. package/dist-server/server/new-bot-defaults.js +115 -0
  204. package/dist-server/server/notify.js +10 -1
  205. package/dist-server/server/org-library.js +1051 -0
  206. package/dist-server/server/package-export.js +627 -39
  207. package/dist-server/server/package-import.js +458 -0
  208. package/dist-server/server/package-parts.js +87 -0
  209. package/dist-server/server/peer-approval.js +16 -7
  210. package/dist-server/server/peer-roster.js +23 -2
  211. package/dist-server/server/presets.js +492 -0
  212. package/dist-server/server/provider-key-check.js +3 -2
  213. package/dist-server/server/published-teams.js +80 -0
  214. package/dist-server/server/recent-work.js +15 -0
  215. package/dist-server/server/request-auth.js +96 -0
  216. package/dist-server/server/room-handoffs.js +44 -14
  217. package/dist-server/server/routes/bot-presets.js +25 -0
  218. package/dist-server/server/routes/hosted-slack.js +20 -0
  219. package/dist-server/server/routes/table.js +14 -0
  220. package/dist-server/server/routine-requests.js +6 -3
  221. package/dist-server/server/routines.js +63 -1
  222. package/dist-server/server/section-context.js +10 -0
  223. package/dist-server/server/skills.js +34 -61
  224. package/dist-server/server/spend.js +111 -16
  225. package/dist-server/server/steer-queue.js +9 -3
  226. package/dist-server/server/store.js +550 -35
  227. package/dist-server/server/surface.js +10 -4
  228. package/dist-server/server/system-prompt.js +12 -4
  229. package/dist-server/server/team-backup.js +16 -3
  230. package/dist-server/server/team-computers.js +10 -0
  231. package/dist-server/server/team-library.js +6 -2
  232. package/dist-server/server/team-manifest.js +4 -3
  233. package/dist-server/server/team-setup-requests.js +16 -5
  234. package/dist-server/server/thread-events.js +17 -1
  235. package/dist-server/server/thread-starters.js +53 -0
  236. package/dist-server/server/turn-context.js +2 -0
  237. package/dist-server/server/turn-watchdog.js +12 -3
  238. package/dist-server/server/usage-ledger.js +43 -9
  239. package/dist-server/server/vps-computer.js +139 -51
  240. package/dist-server/server/vps-container-mcp.js +7 -0
  241. package/dist-server/server/vps-ssh.js +36 -7
  242. package/dist-server/server/workspace-backup-policy.js +21 -2
  243. package/dist-server/server/workspace-backup.js +7 -4
  244. package/dist-server/server/workspace.js +1 -1
  245. package/dist-server/shared/approval-mode.js +1 -1
  246. package/dist-server/shared/ask-question.js +188 -12
  247. package/dist-server/shared/bot-profile.js +15 -0
  248. package/dist-server/shared/command-allowlist.js +1 -0
  249. package/dist-server/shared/credential-request.js +1 -1
  250. package/dist-server/shared/mention-boundary.js +15 -0
  251. package/dist-server/shared/package-format.js +967 -0
  252. package/dist-server/shared/routines.js +7 -1
  253. package/dist-server/shared/skill-md.js +64 -0
  254. package/dist-server/shared/team-backup.js +9 -0
  255. package/dist-server/shared/wire.js +4 -0
  256. package/dist-server/src/lib/composer-attachments.js +16 -0
  257. package/dist-server/vps-container-mcp.js +1147 -179
  258. package/enterprise/README.md +33 -12
  259. package/enterprise/server/index.js +5 -5
  260. package/package.json +1 -1
  261. package/dist/assets/index-Ba9G44HI.js +0 -310
  262. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
2
  import { appendNative } from "./native.js";
3
+ import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
3
4
  const DRIVER_KIND = "boxAgent";
4
5
  // overridable so tests and a dev backend can be pointed at instead of the live provider
5
6
  const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
@@ -11,7 +12,24 @@ const MODELS = {
11
12
  { id: "gpt-5.4", label: "GPT-5.4 (Codex) · on the box" },
12
13
  ],
13
14
  };
14
- /** The box runs every harness ascii.dev ships (claude-code, codex, pi, opencode,
15
+ /** The ask contract appended to every prompt. The box harness cannot pause
16
+ * mid-run, so a question rides the run's final output as a fenced block and
17
+ * OMB parses it at settle — the turn-held transport. */
18
+ const ASK_PROTOCOL = [
19
+ "",
20
+ "## Asking the person a question",
21
+ "When a decision belongs to the person, end your reply with a fenced block exactly like this:",
22
+ "",
23
+ "```omb-ask",
24
+ '{"questions":[{"question":"Ship the release now?","header":"Release","options":[{"label":"Ship now"},{"label":"Wait for the QA signoff"}]}]}',
25
+ "```",
26
+ "",
27
+ "The block must be the last thing in your reply. You may ask up to 6 questions at once, each with up to 12 options; the person can always answer in their own words. Their answers arrive on your next prompt as `Q:`/`A:` lines — never invent them.",
28
+ ].join("\n");
29
+ /** Any fence whose info string names the ask protocol, even when its body
30
+ * does not parse: the marker for "the model tried to ask and failed". */
31
+ const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
32
+ /** The box runs every harness boat.dev ships (claude-code, codex, pi, opencode,
15
33
  * prime-agent, kimi). Which one a model id belongs to comes from the public
16
34
  * catalog, `GET /api/provider-models` at the API root: an object keyed by
17
35
  * harness, each with its `models`. A bot that arrives here from another engine
@@ -35,7 +53,10 @@ const providerFor = (model) => {
35
53
  };
36
54
  function decodeConfig(raw) {
37
55
  const o = (raw ?? {});
38
- return { pollMs: typeof o.pollMs === "number" ? o.pollMs : 2500 };
56
+ return {
57
+ pollMs: typeof o.pollMs === "number" ? o.pollMs : 2500,
58
+ ...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
59
+ };
39
60
  }
40
61
  export const BoxAgentDriver = {
41
62
  driverKind: DRIVER_KIND,
@@ -48,6 +69,13 @@ export const BoxAgentDriver = {
48
69
  const token = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN ?? "";
49
70
  const listeners = new Set();
50
71
  const active = new Map();
72
+ /** One open ask per thread: the turn-held transport's pending card. The
73
+ * thread is busy for exactly as long as the ask is open, so a second
74
+ * block can never race the first. */
75
+ const heldAsks = new Map();
76
+ /** Threads whose last run ended with an unparseable omb-ask fence: the
77
+ * next prompt carries the correction so the ask is never lost silently. */
78
+ const malformedAsks = new Set();
51
79
  const emit = (event) => {
52
80
  for (const l of Array.from(listeners))
53
81
  l(event);
@@ -84,46 +112,65 @@ export const BoxAgentDriver = {
84
112
  throw new Error("a turn is already running on this thread");
85
113
  const turnId = newId();
86
114
  const model = turn.model || MODELS.default;
115
+ const correction = malformedAsks.delete(threadId)
116
+ ? "Your previous omb-ask block was malformed or empty, so the person never saw it. Ask again with a valid fenced omb-ask JSON block, or ask in plain words."
117
+ : "";
87
118
  const prompt = [
88
119
  turn.system,
89
120
  "You are working on the assigned cloud computer — use its desktop, Chrome, and shell within the access described above.",
121
+ ASK_PROTOCOL,
122
+ ...(correction ? [correction] : []),
90
123
  "",
91
124
  turn.text,
92
125
  ]
93
126
  .filter((s) => s !== undefined)
94
127
  .join("\n");
95
- if (!catalog)
96
- await loadCatalog();
97
- const started = await api(`/boxes/${boxId}/prompt`, {
98
- method: "POST",
99
- body: JSON.stringify({ ...providerFor(model), prompt }),
100
- });
101
- appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt, response: started } });
102
- // real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
103
- // id:<box id>} — never fall back to the bare id, it's the box's
104
- const promptId = started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
128
+ const postPrompt = async (promptText) => {
129
+ if (!catalog)
130
+ await loadCatalog();
131
+ const started = await api(`/boxes/${boxId}/prompt`, {
132
+ method: "POST",
133
+ body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
134
+ });
135
+ appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
136
+ // real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
137
+ // id:<box id>} — never fall back to the bare id, it's the box's
138
+ return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
139
+ };
140
+ const promptId = await postPrompt(prompt);
105
141
  let cancelled = false;
106
142
  active.set(threadId, {
107
143
  turnId,
108
144
  boxId,
109
145
  cancel: () => {
110
146
  cancelled = true;
147
+ heldAsks.get(threadId)?.settle(null, "system");
111
148
  void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
112
149
  },
113
150
  });
114
151
  emit({ ...base(threadId, turnId), type: "turn.started" });
115
152
  emit({ ...base(threadId, turnId), type: "session.started", sessionId: promptId, model });
116
- // poll events + run status until the prompt settles
153
+ // poll events + run status until the prompt settles — and, when the
154
+ // run ends on an omb-ask block, until the person answers it: the OMB
155
+ // turn is the unit the whole server already understands (busy thread,
156
+ // waiting-on-you, routines waiting), so it stays open across the ask
157
+ // and the continuation prompt continues it rather than starting a
158
+ // driver-initiated turn nobody accounts for.
117
159
  (async () => {
160
+ // Event ids seen this TURN. The events stream is the whole
161
+ // conversation, so a continuation run must not re-ingest history;
162
+ // `taskId` normally filters it, but ids survive even shape drift.
118
163
  const seen = new Set();
119
- const startedAt = Date.now();
120
164
  let lastText = "";
121
165
  let pendingText = "";
122
166
  /** Why the box could not answer (login expired, model refused, …). */
123
167
  let problem = null;
124
- /** Emit unflushed deltas as assistant_text and reset pendingText. */
168
+ /** Emit unflushed deltas as assistant_text and reset pendingText.
169
+ * The omb-ask block is protocol, not prose: it streamed raw (the
170
+ * card is its readable form), and the settled message shows the
171
+ * words around it, never the JSON. */
125
172
  const flushAssistantText = () => {
126
- const text = pendingText;
173
+ const text = stripOmbAskBlock(pendingText);
127
174
  pendingText = "";
128
175
  if (!text.trim())
129
176
  return;
@@ -138,7 +185,88 @@ export const BoxAgentDriver = {
138
185
  pendingText += delta;
139
186
  emit({ ...base(threadId, turnId), type: "content.delta", streamKind: "assistant_text", delta });
140
187
  };
141
- try {
188
+ /** The run has settled: flush its prose, then hold the OMB turn open
189
+ * on an omb-ask block (if any) until the person answers or the ask
190
+ * times out. An answer chains the continuation run under this same
191
+ * turn; a deny or timeout ends it. */
192
+ const finishRun = async (ok, stopReason) => {
193
+ flushAssistantText();
194
+ // An interrupt that lands between ask text streaming and settle
195
+ // finds no heldAsks entry to settle; without this check the run
196
+ // would register a fresh ask for a dead turn and hold stop pending
197
+ // for the ask timeout.
198
+ if (cancelled) {
199
+ return { ok: false, stopReason: "interrupted" };
200
+ }
201
+ // A remotely interrupted or failed run must stay stopped, even
202
+ // if its partial output already contains a complete question.
203
+ if (!ok)
204
+ return { ok, stopReason };
205
+ const questions = parseOmbAskQuestions(lastText);
206
+ if (!questions) {
207
+ // A fence that did not parse is a question the person never saw.
208
+ // Say so on the next prompt instead of losing it silently. (An
209
+ // over-cap block does not land here: the parser caps it at six
210
+ // questions and still shows the card.)
211
+ if (ASK_FENCE_ANY.test(lastText))
212
+ malformedAsks.add(threadId);
213
+ return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
214
+ }
215
+ const requestId = newId();
216
+ let settleHeld;
217
+ const held = new Promise((resolve) => {
218
+ const timer = setTimeout(() => settleHeld(null, "timeout"), config.askTimeoutMs ?? 15 * 60_000);
219
+ timer.unref?.();
220
+ settleHeld = (reply, source) => {
221
+ heldAsks.delete(threadId);
222
+ clearTimeout(timer);
223
+ resolve([reply, source]);
224
+ };
225
+ });
226
+ // register before emitting — an answer can race the emit
227
+ heldAsks.set(threadId, { requestId, settle: settleHeld });
228
+ const choices = questionChoices(questions);
229
+ emit({
230
+ ...base(threadId, turnId),
231
+ requestId,
232
+ type: "request.opened",
233
+ requestType: "question",
234
+ tool: OMB_ASK_TOOL,
235
+ summary: askQuestionSummary(questions),
236
+ questions,
237
+ ...(choices?.length ? { choices } : {}),
238
+ origin: "output",
239
+ });
240
+ const [reply, source] = await held;
241
+ emit({ ...base(threadId, turnId), requestId, type: "request.resolved", behavior: reply !== null ? "answer" : "deny", source });
242
+ if (reply === null || cancelled) {
243
+ return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
244
+ }
245
+ // The turn is still open, so this prompt continues it: same turnId,
246
+ // same accounting, and the answer reaches the box the way the ask
247
+ // contract promised — Q:/A: blocks, capped for echo.
248
+ const continuation = [
249
+ capAnswerEcho(answerWithoutPreamble(reply)),
250
+ "",
251
+ "The person answered the omb-ask questions above (Q:/A:). Continue the task with their answers; end with another omb-ask block only if you truly need more.",
252
+ ].join("\n");
253
+ const nextPromptId = await postPrompt(continuation);
254
+ // Stop can land while the continuation POST is in flight: the
255
+ // interrupt inside cancel() then hits a box with no active run,
256
+ // and the continuation would start after it. Interrupt the run
257
+ // that just started before ending the turn.
258
+ if (cancelled) {
259
+ void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
260
+ return { ok: false, stopReason: "interrupted" };
261
+ }
262
+ return await settleRun(nextPromptId);
263
+ };
264
+ /** Poll one box run to its settle. */
265
+ const settleRun = async (runPromptId) => {
266
+ const startedAt = Date.now(); // one 30-min ceiling per box run, ask chains included
267
+ lastText = "";
268
+ pendingText = "";
269
+ problem = null;
142
270
  for (;;) {
143
271
  if (cancelled)
144
272
  break;
@@ -149,7 +277,7 @@ export const BoxAgentDriver = {
149
277
  // The stream is the whole conversation from its start; `taskId`
150
278
  // names the prompt run an event belongs to. Earlier turns are
151
279
  // history, not this answer.
152
- if (promptId && ev.taskId && String(ev.taskId) !== promptId)
280
+ if (runPromptId && ev.taskId && String(ev.taskId) !== runPromptId)
153
281
  continue;
154
282
  const id = String(ev.id ?? ev.eventId ?? JSON.stringify(ev).slice(0, 120));
155
283
  if (seen.has(id))
@@ -184,16 +312,13 @@ export const BoxAgentDriver = {
184
312
  // shape-drift backstop: without a promptId the status poll
185
313
  // below can never see a terminal state, so settle off the
186
314
  // events themselves instead of hanging to the 30-min ceiling
187
- if (!promptId && /complete|finish|done|success|fail|error/i.test(kind)) {
188
- active.delete(threadId);
189
- flushAssistantText();
315
+ if (!runPromptId && /complete|finish|done|success|fail|error/i.test(kind)) {
190
316
  const failed = /fail|error/i.test(kind);
191
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: !failed, stopReason: failed ? kind : null, cost: null });
192
- return;
317
+ return await finishRun(!failed, failed ? kind : null);
193
318
  }
194
319
  }
195
- if (promptId) {
196
- const status = await api(`/boxes/${boxId}/prompts/${promptId}`).catch(() => null);
320
+ if (runPromptId) {
321
+ const status = await api(`/boxes/${boxId}/prompts/${runPromptId}`).catch(() => null);
197
322
  appendNative(threadId, { dir: "in", source: "box.prompt.status", msg: status });
198
323
  // real shape (2026-08): {promptRun:{status:"finished",…}} —
199
324
  // flat fallbacks kept for drift
@@ -211,19 +336,13 @@ export const BoxAgentDriver = {
211
336
  throw new Error(problem);
212
337
  pendingText = "(finished)";
213
338
  }
214
- flushAssistantText();
215
- active.delete(threadId);
216
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: true, stopReason: null, cost: null });
217
- return;
339
+ return await finishRun(true, null);
218
340
  }
219
341
  if (/failed|error|cancelled|interrupted/i.test(state)) {
220
342
  const runError = [run?.error, run?.failureReason, run?.message].find((v) => typeof v === "string" && v.trim());
221
343
  if (problem || runError || /failed|error/i.test(state))
222
344
  throw new Error(problem ?? runError ?? `the box run ${state}`);
223
- flushAssistantText();
224
- active.delete(threadId);
225
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: state, cost: null });
226
- return;
345
+ return await finishRun(false, state);
227
346
  }
228
347
  }
229
348
  if (Date.now() - startedAt > 30 * 60_000) {
@@ -231,12 +350,16 @@ export const BoxAgentDriver = {
231
350
  }
232
351
  }
233
352
  // cancelled
234
- flushAssistantText();
353
+ return await finishRun(false, "interrupted");
354
+ };
355
+ try {
356
+ const outcome = await settleRun(promptId);
235
357
  active.delete(threadId);
236
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: "interrupted", cost: null });
358
+ emit({ ...base(threadId, turnId), type: "turn.completed", ok: outcome.ok, stopReason: outcome.stopReason, cost: null });
237
359
  }
238
360
  catch (e) {
239
361
  flushAssistantText();
362
+ heldAsks.delete(threadId);
240
363
  active.delete(threadId);
241
364
  emit({ ...base(threadId, turnId), type: "runtime.error", message: e.message });
242
365
  emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: "error", cost: null });
@@ -268,7 +391,22 @@ export const BoxAgentDriver = {
268
391
  capabilities: { sessionModelSwitch: "in-session" },
269
392
  sendTurn,
270
393
  interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
271
- respondToRequest: async () => "unavailable", // this engine has no asks to answer
394
+ respondToRequest: async (threadId, requestId, decision) => {
395
+ const held = heldAsks.get(threadId);
396
+ if (!held || held.requestId !== requestId)
397
+ return "unavailable"; // settled, timed out, or restarted away
398
+ const reply = decision.message?.trim();
399
+ if (decision.behavior === "answer" && reply) {
400
+ held.settle(decision.message, "user");
401
+ return "answered";
402
+ }
403
+ if (decision.behavior === "deny") {
404
+ held.settle(null, "user");
405
+ return "rejected";
406
+ }
407
+ // "allow" is not an answer to a question, and blank text is not either
408
+ return "unavailable";
409
+ },
272
410
  hasSession: (threadId) => active.has(threadId),
273
411
  stopAll: async () => {
274
412
  for (const { cancel } of active.values())
@@ -14,6 +14,7 @@ import { CustomAcpDriver } from "./acp/custom.js";
14
14
  import { HermesAgentDriver } from "./acp/hermes.js";
15
15
  import { OpenAICompatDriver } from "./openai-compat.js";
16
16
  import { PiDriver } from "./pi.js";
17
+ import { MistralDriver } from "./mistral.js";
17
18
  import { MinimaxDriver } from "./minimax.js";
18
19
  export const BUILT_IN_DRIVERS = [
19
20
  GrokDriver,
@@ -33,4 +34,5 @@ export const BUILT_IN_DRIVERS = [
33
34
  AntigravityDriver,
34
35
  BoxAgentDriver,
35
36
  MinimaxDriver,
37
+ MistralDriver,
36
38
  ];
@@ -0,0 +1,90 @@
1
+ import { isolatedRemoteCommand, runCommand, screenshotBox } from "../box.js";
2
+ import { createControlClient, CONTROL_REFUSAL_PLAIN } from "../control-client.js";
3
+ const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
4
+ const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
5
+ const TOOLS = [
6
+ tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
7
+ tool("get_screen_size", "Get the cloud desktop width and height."),
8
+ tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
9
+ tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
10
+ tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
11
+ tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
12
+ tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
13
+ tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
14
+ tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
15
+ tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
16
+ ];
17
+ const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
18
+ const buttons = { left: 1, middle: 2, right: 3 };
19
+ const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
20
+ /** Uses only the leased descriptor supplied by the harness. Schema validation
21
+ * and user approval happen in the shared chat tool executor before this call. */
22
+ export class ChatBoxClient {
23
+ closed = false;
24
+ control;
25
+ descriptor;
26
+ constructor(descriptor) {
27
+ this.descriptor = descriptor;
28
+ if (!descriptor.control?.url || !descriptor.control.token)
29
+ throw new Error("Cloud computer control gate missing");
30
+ this.control = createControlClient(descriptor.control);
31
+ }
32
+ async tools() { return TOOLS; }
33
+ async close() { this.closed = true; }
34
+ async call(_method, params, signal) {
35
+ signal.throwIfAborted();
36
+ if (this.closed)
37
+ throw new Error("Cloud computer session closed");
38
+ const state = await this.control.state(true);
39
+ signal.throwIfAborted();
40
+ if (this.closed)
41
+ throw new Error("Cloud computer session closed");
42
+ if (state.held || state.blockedReason)
43
+ return { isError: true, content: [{ type: "text", text: state.blockedReason || CONTROL_REFUSAL_PLAIN }] };
44
+ const { name, arguments: a } = params;
45
+ const config = { box: { token: this.descriptor.token } };
46
+ const id = this.descriptor.boxId;
47
+ if (name === "screenshot") {
48
+ const shot = await screenshotBox(config, "", id, { signal, nativeSize: true });
49
+ signal.throwIfAborted();
50
+ return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
51
+ }
52
+ const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
53
+ let command;
54
+ switch (name) {
55
+ case "get_screen_size":
56
+ command = "xdotool getdisplaygeometry";
57
+ break;
58
+ case "click":
59
+ command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
60
+ break;
61
+ case "move":
62
+ command = mouse;
63
+ break;
64
+ case "drag":
65
+ command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
66
+ break;
67
+ case "type_text":
68
+ command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
69
+ break;
70
+ case "key_press":
71
+ command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
72
+ break;
73
+ case "scroll":
74
+ command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
75
+ break;
76
+ case "open_url":
77
+ command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
78
+ break;
79
+ case "exec":
80
+ command = String(a.command);
81
+ break;
82
+ default: throw new Error("Unknown cloud computer tool");
83
+ }
84
+ const result = await runCommand(config, id, isolatedRemoteCommand(command), { signal });
85
+ signal.throwIfAborted();
86
+ if (name === "open_url" && result.ok)
87
+ return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
88
+ return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
89
+ }
90
+ }
@@ -0,0 +1,54 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ const IMAGE_BYTES = 20 * 1024 * 1024;
3
+ const MIME = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
4
+ export const CHAT_IMAGE_BUDGET = 32 * 1024 * 1024;
5
+ /** Bound retained base64 before JSON serialization or another model call. */
6
+ export function chatImageBudget() {
7
+ let used = 0;
8
+ return (parts) => {
9
+ if (!Array.isArray(parts))
10
+ return;
11
+ const added = parts.reduce((total, part) => total + (part.type === "image_url" ? part.image_url.url.length : 0), 0);
12
+ if (used + added > CHAT_IMAGE_BUDGET)
13
+ throw new Error("Turn image budget exceeded (32 MiB encoded); screenshot cannot be retained. An operation may already have taken effect; inspect its state before retrying.");
14
+ used += added;
15
+ };
16
+ }
17
+ export function assertImageTransport(url) {
18
+ const endpoint = new URL(url);
19
+ const loopback = endpoint.hostname === "localhost" || endpoint.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
20
+ if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && loopback)) {
21
+ throw new Error("Images and computer tools require HTTPS for remote API endpoints; HTTP is supported only on loopback.");
22
+ }
23
+ }
24
+ export function chatImage(item) {
25
+ const { mimeType, data } = item;
26
+ if (typeof mimeType !== "string" || !MIME.has(mimeType) || typeof data !== "string" ||
27
+ !data.length || data.length > Math.ceil(IMAGE_BYTES / 3) * 4 || data.length % 4 ||
28
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
29
+ throw new Error("Invalid or oversized MCP image");
30
+ }
31
+ const bytes = Buffer.from(data, "base64");
32
+ const matches = mimeType === "image/png" ? bytes.length >= 33 && bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")) && bytes.toString("ascii", 12, 16) === "IHDR"
33
+ : mimeType === "image/jpeg" ? bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff
34
+ : mimeType === "image/gif" ? bytes.length >= 13 && ["GIF87a", "GIF89a"].includes(bytes.toString("ascii", 0, 6))
35
+ : bytes.length >= 16 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP";
36
+ if (bytes.length > IMAGE_BYTES || bytes.toString("base64") !== data || !matches)
37
+ throw new Error("Image bytes do not match the declared MIME type");
38
+ return { type: "image_url", image_url: { url: `data:${mimeType};base64,${data}` } };
39
+ }
40
+ export function chatUserContent(turn) {
41
+ if (!turn.images?.length)
42
+ return turn.text;
43
+ const budget = chatImageBudget();
44
+ return [{ type: "text", text: turn.text }, ...turn.images.map(image => {
45
+ if (statSync(image.path).size > IMAGE_BYTES)
46
+ throw new Error("Image exceeds 20 MB");
47
+ const part = chatImage({ mimeType: image.mime, data: readFileSync(image.path).toString("base64") });
48
+ budget([part]);
49
+ return part;
50
+ })];
51
+ }
52
+ export function chatToolImages(callId, images) {
53
+ return images?.length ? [{ type: "text", text: `Screenshot result from tool call ${callId}:` }, ...images] : [];
54
+ }