openmausbot 0.1.86 → 0.1.88

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 (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -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
+ }
@@ -6,6 +6,8 @@ import formats from "ajv-formats";
6
6
  import { stripControlPlaneEnv } from "../config.js";
7
7
  import { augmentedPath } from "../env-path.js";
8
8
  import { killCliTree, spawnCli } from "../procs.js";
9
+ import { chatImage } from "./chat-images.js";
10
+ import { ChatBoxClient } from "./chat-box-tools.js";
9
11
  /** The transport cannot safely continue this turn. A dispatched operation may
10
12
  * already have taken effect, so callers must not retry it through a new round. */
11
13
  export class ChatToolSessionError extends Error {
@@ -32,6 +34,7 @@ export function chatMcpEnvironment(serverEnv, source = process.env) {
32
34
  return { ...env, ...serverEnv };
33
35
  }
34
36
  class ChatMcpClient {
37
+ frameBytes;
35
38
  child;
36
39
  buffer = "";
37
40
  nextId = 1;
@@ -39,7 +42,8 @@ class ChatMcpClient {
39
42
  closed = false;
40
43
  closing;
41
44
  pending = new Map();
42
- constructor(server) {
45
+ constructor(server, computerUse = false) {
46
+ this.frameBytes = computerUse ? 32 * 1024 * 1024 : FRAME_BYTES;
43
47
  try {
44
48
  // The desktop shell inherits Finder's bare PATH, where `npx`-style
45
49
  // servers cannot find `node` and exit at once. Widen it the way the
@@ -89,7 +93,7 @@ class ChatMcpClient {
89
93
  if (this.closed || !this.child.stdin.writable || this.child.stdin.destroyed)
90
94
  throw new Error("MCP session closed");
91
95
  const encoded = JSON.stringify(frame) + "\n";
92
- if (Buffer.byteLength(encoded) > FRAME_BYTES)
96
+ if (Buffer.byteLength(encoded) > this.frameBytes)
93
97
  throw new Error("MCP request exceeds the frame limit");
94
98
  this.child.stdin.write(encoded);
95
99
  }
@@ -103,7 +107,7 @@ class ChatMcpClient {
103
107
  this.buffer = this.buffer.slice(newline + 1);
104
108
  if (++this.frames > MAX_FRAMES)
105
109
  return this.fail(new Error("MCP session exceeded the response frame count limit"));
106
- if (Buffer.byteLength(line) > FRAME_BYTES)
110
+ if (Buffer.byteLength(line) > this.frameBytes)
107
111
  return this.fail(new Error("MCP response exceeds the frame limit"));
108
112
  if (!line.trim())
109
113
  continue;
@@ -140,7 +144,7 @@ class ChatMcpClient {
140
144
  else
141
145
  entry.resolve(message.result);
142
146
  }
143
- if (Buffer.byteLength(this.buffer) > FRAME_BYTES)
147
+ if (Buffer.byteLength(this.buffer) > this.frameBytes)
144
148
  this.fail(new Error("MCP response exceeds the frame limit"));
145
149
  }
146
150
  async call(method, params, signal, timeout) {
@@ -244,6 +248,8 @@ function compileSchema(schema) {
244
248
  // ajv-formats is CommonJS and exports the plugin as both module.exports
245
249
  // and .default; the latter also matches its NodeNext declaration.
246
250
  formats.default(compiler);
251
+ compiler.addFormat("uint32", { type: "number", validate: value => Number.isInteger(value) && value >= 0 && value <= 4294967295 });
252
+ compiler.addFormat("uint64", { type: "number", validate: value => Number.isSafeInteger(value) && value >= 0 });
247
253
  try {
248
254
  return compiler.compile(schema);
249
255
  }
@@ -260,8 +266,14 @@ function boundedText(value) {
260
266
  end -= 1;
261
267
  return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
262
268
  }
263
- export async function mountChatTools(integrations, signal) {
269
+ export async function mountChatTools(integrations, signal, computerUse = false) {
264
270
  const servers = [];
271
+ if (computerUse && integrations?.computer)
272
+ servers.push(["computer", integrations.computer]);
273
+ if (computerUse && integrations?.localComputer)
274
+ servers.push(["computer", integrations.localComputer]);
275
+ if (computerUse && integrations?.browser)
276
+ servers.push(["browser", integrations.browser]);
265
277
  if (integrations?.agents)
266
278
  servers.push(["agents", integrations.agents]);
267
279
  if (integrations?.composio)
@@ -300,7 +312,9 @@ export async function mountChatTools(integrations, signal) {
300
312
  const mounts = await Promise.allSettled(servers.map(async ([name, descriptor]) => {
301
313
  if (signal.aborted || closed)
302
314
  throw aborted();
303
- const client = new ChatMcpClient(descriptor);
315
+ // Every mounted MCP server can return images when the caller enables
316
+ // image delivery, including custom servers. Text stays bounded below.
317
+ const client = "boxId" in descriptor ? new ChatBoxClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
304
318
  clients.push(client);
305
319
  return { name, client, tools: await client.tools(signal) };
306
320
  }));
@@ -320,6 +334,18 @@ export async function mountChatTools(integrations, signal) {
320
334
  if (Buffer.byteLength(JSON.stringify(tool.inputSchema)) > SCHEMA_BYTES)
321
335
  throw new Error("MCP tool schema exceeds the 64KB limit");
322
336
  const schema = compileSchema(tool.inputSchema);
337
+ const parameters = { ...tool.inputSchema };
338
+ const constraints = {};
339
+ if (computerUse) {
340
+ for (const key of ["anyOf", "oneOf", "allOf", "not"]) {
341
+ if (key in parameters) {
342
+ constraints[key] = parameters[key];
343
+ delete parameters[key];
344
+ }
345
+ }
346
+ }
347
+ const description = (typeof tool.description === "string" ? tool.description : "Configured MCP tool") +
348
+ (Object.keys(constraints).length ? " Additional argument constraints (validated before execution): " + JSON.stringify(constraints) : "");
323
349
  const base = `${server}_${tool.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "mcp_tool";
324
350
  let name = base;
325
351
  for (let index = 2; registered.has(name); index += 1) {
@@ -327,7 +353,7 @@ export async function mountChatTools(integrations, signal) {
327
353
  name = base.slice(0, 64 - suffix.length) + suffix;
328
354
  }
329
355
  registered.set(name, { client, name: tool.name, schema });
330
- definitions.push({ type: "function", function: { name, description: typeof tool.description === "string" ? tool.description : "Configured MCP tool", parameters: tool.inputSchema } });
356
+ definitions.push({ type: "function", function: { name, description, parameters } });
331
357
  if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
332
358
  throw new Error("MCP tool catalog exceeds the 1MB limit");
333
359
  }
@@ -364,12 +390,15 @@ export async function mountChatTools(integrations, signal) {
364
390
  if (!object(result) || !Array.isArray(result.content) || (result.isError !== undefined && typeof result.isError !== "boolean"))
365
391
  throw new Error("MCP tool returned an invalid result; execution outcome may be uncertain");
366
392
  const parts = [];
393
+ const images = [];
367
394
  let unsupported = 0;
368
395
  for (const item of result.content) {
369
396
  if (!object(item) || typeof item.type !== "string" || (item.type === "text" && typeof item.text !== "string"))
370
397
  throw new Error("MCP tool returned invalid content; execution outcome may be uncertain");
371
398
  if (item.type === "text")
372
399
  parts.push(item.text);
400
+ else if (computerUse && item.type === "image")
401
+ images.push(chatImage(item));
373
402
  else
374
403
  unsupported += 1;
375
404
  }
@@ -377,7 +406,8 @@ export async function mountChatTools(integrations, signal) {
377
406
  parts.push(JSON.stringify(result.structuredContent));
378
407
  if (unsupported)
379
408
  parts.unshift(`[${unsupported} unsupported MCP content item(s) omitted. The operation may have taken effect, but its full result cannot be represented; inspect its state before retrying.]`);
380
- return { text: boundedText(parts.join("\n") || "(empty result)"), ok: result.isError !== true && unsupported === 0 };
409
+ return { text: boundedText(parts.join("\n") || (images.length ? "Screenshot captured." : "(empty result)")), ok: result.isError !== true && unsupported === 0,
410
+ ...(images.length ? { images } : {}) };
381
411
  }
382
412
  catch (error) {
383
413
  await close();
@@ -1,4 +1,4 @@
1
- // Per-turn permission gate, adapted from the approval lifecycle in #780.
1
+ // Per-turn ask gate, adapted from the approval lifecycle in #780.
2
2
  // Register before publishing: a harness listener may answer synchronously.
3
3
  import { newId } from "../contracts.js";
4
4
  export function createChatToolApproval(options) {
@@ -10,33 +10,85 @@ export function createChatToolApproval(options) {
10
10
  return Promise.resolve(false);
11
11
  const ask = { id: newId(), tool, summary };
12
12
  return new Promise((resolve) => {
13
- const finish = (allowed, source) => {
14
- if (!pending.delete(ask.id))
15
- return;
16
- clearTimeout(timer);
17
- options.signal.removeEventListener("abort", abort);
18
- options.resolved(ask, allowed, source);
19
- resolve(allowed);
13
+ let timer;
14
+ const card = {
15
+ kind: "permission",
16
+ ask,
17
+ finish(source, decision) {
18
+ if (!pending.delete(ask.id))
19
+ return;
20
+ clearTimeout(timer);
21
+ options.signal.removeEventListener("abort", abort);
22
+ const allowed = decision?.allowed === true;
23
+ options.resolved(ask, allowed, source);
24
+ resolve(allowed);
25
+ },
20
26
  };
21
- const abort = () => finish(false, "system");
22
- const timer = setTimeout(() => finish(false, "timeout"), options.timeoutMs ?? 15 * 60_000);
27
+ const abort = () => card.finish("system");
28
+ timer = setTimeout(() => card.finish("timeout"), options.timeoutMs ?? 15 * 60_000);
23
29
  timer.unref?.();
24
- pending.set(ask.id, finish);
30
+ pending.set(ask.id, card);
25
31
  options.signal.addEventListener("abort", abort, { once: true });
26
32
  options.open(ask);
27
33
  });
28
34
  },
29
- answer(id, behavior) {
30
- const finish = pending.get(id);
31
- if (!finish || options.signal.aborted || behavior === "answer")
35
+ question(tool, summary, questions) {
36
+ if (closed || options.signal.aborted)
37
+ return Promise.resolve(null);
38
+ const ask = { id: newId(), tool, summary };
39
+ return new Promise((resolve) => {
40
+ let timer;
41
+ const card = {
42
+ kind: "question",
43
+ ask,
44
+ finish(source, decision) {
45
+ if (!pending.delete(ask.id))
46
+ return;
47
+ clearTimeout(timer);
48
+ options.signal.removeEventListener("abort", abort);
49
+ // Only the person's reply resolves a question; a timeout or
50
+ // abort resolves null so no system note can occupy the answer
51
+ // slot the model reads as the person's words.
52
+ const reply = typeof decision?.message === "string" && decision.message.length > 0 ? decision.message : null;
53
+ options.resolvedQuestion(ask, reply !== null, source);
54
+ resolve(reply);
55
+ },
56
+ };
57
+ const abort = () => card.finish("system");
58
+ timer = setTimeout(() => card.finish("timeout"), options.timeoutMs ?? 15 * 60_000);
59
+ timer.unref?.();
60
+ pending.set(ask.id, card);
61
+ options.signal.addEventListener("abort", abort, { once: true });
62
+ options.openQuestion(ask, questions);
63
+ });
64
+ },
65
+ answer(id, behavior, message) {
66
+ const card = pending.get(id);
67
+ if (!card || options.signal.aborted)
32
68
  return "unavailable";
33
- finish(behavior === "allow", "user");
34
- return behavior === "allow" ? "allowed-once" : "rejected";
69
+ if (behavior === "deny") {
70
+ card.finish("user");
71
+ return "rejected";
72
+ }
73
+ if (card.kind === "permission") {
74
+ // An answer is not a permission: only allow or deny settles it.
75
+ if (behavior !== "allow")
76
+ return "unavailable";
77
+ card.finish("user", { allowed: true });
78
+ return "allowed-once";
79
+ }
80
+ // And an allow is not an answer: a question settles only on a real
81
+ // reply, never a blank the model would have to interpret.
82
+ if (behavior === "answer" && typeof message === "string" && message.trim()) {
83
+ card.finish("user", { message });
84
+ return "answered";
85
+ }
86
+ return "unavailable";
35
87
  },
36
88
  close() {
37
89
  closed = true;
38
- for (const finish of pending.values())
39
- finish(false, "system");
90
+ for (const card of pending.values())
91
+ card.finish("system");
40
92
  },
41
93
  };
42
94
  }
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Integrations become MCP servers on the CLI:
8
8
  // - Composio Sessions (connected apps → tools) over streamable HTTP
9
- // - the bot's cloud computer (box.ascii.dev) via server/computer-proxy.ts
9
+ // - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
10
10
  // — screenshot/exec/open_url, the CUA-on-the-box bridge
11
11
  import { createHash, randomBytes } from "node:crypto";
12
12
  import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
@@ -23,8 +23,11 @@ import { gateServer, resultBudget } from "../mcp-gate-config.js";
23
23
  import { newEventId, newId } from "../contracts.js";
24
24
  import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summary.js";
25
25
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
26
+ import { sessionIdlePolicy } from "./session-idle.js";
27
+ import { parseVersionTriple, versionAtLeast } from "./acp/core.js";
26
28
  import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
27
29
  import { appendNative } from "./native.js";
30
+ import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
28
31
  import { SPAWNED_PROXIES } from "../proxy-paths.js";
29
32
  import { extractMcpImages } from "../mcp-tool-images.js";
30
33
  import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
@@ -108,6 +111,17 @@ export function claudeAuthFailure(frame, text) {
108
111
  return false;
109
112
  return frame.error === "authentication_failed" || classifyError({ text }).reason === "auth";
110
113
  }
114
+ /** A model newer than the installed Claude Code: the API refuses it and the
115
+ * CLI relays that as an api-error frame ("Claude Code 2.1.268 does not
116
+ * support this model; version 2.1.280 or newer is required. Run 'claude
117
+ * update'…"). It names no model, so it covers every model it happens for.
118
+ * Like a signed-out turn, it is fixed by changing the install, not by a
119
+ * retry, so the UI offers to run the update. */
120
+ export function claudeVersionTooOld(frame, text) {
121
+ if (frame.is_api_error_message !== true && typeof frame.error !== "string")
122
+ return false;
123
+ return /\bClaude Code v?\d+(?:\.\d+)+ does not support this model\b/i.test(text);
124
+ }
111
125
  /** The CLI environment shared by auth probes and real turns.
112
126
  *
113
127
  * Subscription users can be billed pay-as-you-go if an inherited API key
@@ -274,17 +288,7 @@ export const CLAUDE_CONTEXT_CONTROL_MIN_VERSION = CLAUDE_FLAG_FLOORS["--system-p
274
288
  * is the version. Null when nothing parses, e.g. a wrapper that prints its
275
289
  * own banner first — see claudeCliSupports for how that is treated. */
276
290
  export function parseClaudeCliVersion(stdout) {
277
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout ?? "");
278
- if (!match)
279
- return null;
280
- return [Number(match[1]), Number(match[2]), Number(match[3])];
281
- }
282
- function versionAtLeast(installed, floor) {
283
- for (let i = 0; i < 3; i += 1) {
284
- if (installed[i] !== floor[i])
285
- return installed[i] > floor[i];
286
- }
287
- return true;
291
+ return parseVersionTriple(stdout ?? "");
288
292
  }
289
293
  /** Whether a CLI reporting `version` accepts `flag`. A version that could
290
294
  * not be parsed counts as current: every CLI that predates a floor prints a
@@ -856,11 +860,7 @@ export const ClaudeDriver = {
856
860
  // one active turn per thread; a second send while busy is a caller bug
857
861
  const active = new Map();
858
862
  const sessions = new Map();
859
- const configuredIdleMinimum = Number(process.env.OMB_CLAUDE_SESSION_IDLE_MIN_MS);
860
- const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
861
- ? configuredIdleMinimum
862
- : 10_000;
863
- const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
863
+ const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("CLAUDE");
864
864
  const stopSession = (session) => {
865
865
  void killCliTree(session.child).then((stopped) => {
866
866
  if (stopped)
@@ -1199,6 +1199,7 @@ export const ClaudeDriver = {
1199
1199
  // parallel bot work must use the harness's durable delegate_bot path.
1200
1200
  env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
1201
1201
  const cwd = turn.cwd ?? homedir();
1202
+ const commandCwd = permissionLaunchCwd(cwd);
1202
1203
  // Everything that shapes the process, minus session/turn-specific temp
1203
1204
  // paths. Their contents are represented directly in the key instead.
1204
1205
  const privateFileFlags = new Set(["--mcp-config", "--settings"]);
@@ -1239,7 +1240,7 @@ export const ClaudeDriver = {
1239
1240
  }, turnId, broker: live.broker });
1240
1241
  emit({ ...base(threadId, turnId), type: "turn.started" });
1241
1242
  const volatile = turn.systemVolatile ?? "";
1242
- const message = volatile === live.volatile
1243
+ const message = volatile === live.volatile && !turn.mentionTurn
1243
1244
  ? promptMsg
1244
1245
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1245
1246
  live.volatile = volatile;
@@ -1323,6 +1324,9 @@ export const ClaudeDriver = {
1323
1324
  requestType: ask.kind,
1324
1325
  tool: ask.tool,
1325
1326
  summary: askSummary(ask),
1327
+ command: ask.kind === "permission" && ask.tool === "Bash"
1328
+ ? permissionCommand(ask.input.command, commandCwd) : undefined,
1329
+ requiresExplicitApproval: ask.kind === "permission" && ask.tool === "Bash" && ask.input.dangerouslyDisableSandbox === true || undefined,
1326
1330
  nativeReview,
1327
1331
  // the proxy hands Claude its own suggested rules on `always`;
1328
1332
  // host control stays one action at a time
@@ -1441,6 +1445,11 @@ export const ClaudeDriver = {
1441
1445
  // A settled turn owns no retry budget. Retained CLI sessions may run
1442
1446
  // many later turns on this thread, and each must start fresh.
1443
1447
  retryState.delete(threadId);
1448
+ // Updating the executable cannot update code already loaded by this
1449
+ // pooled child. Retire it before announcing completion so an explicit
1450
+ // retry resumes on a fresh process; healthy sibling sessions stay warm.
1451
+ if (stopReason === "update_required")
1452
+ closeSession(threadId, "update required");
1444
1453
  emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
1445
1454
  if (session.child.exitCode === null && !session.closing)
1446
1455
  armIdle(threadId);
@@ -1503,6 +1512,12 @@ export const ClaudeDriver = {
1503
1512
  emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: text, setup: true });
1504
1513
  break;
1505
1514
  }
1515
+ if (claudeVersionTooOld(o, text)) {
1516
+ if (session.turn)
1517
+ session.turn.updateRequired = true;
1518
+ emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: text, setup: true, claudeUpdate: true });
1519
+ break;
1520
+ }
1506
1521
  if (text.trim()) {
1507
1522
  // The CLI's own report of any other API error is still shown,
1508
1523
  // but marked: the model never produced it.
@@ -1565,7 +1580,11 @@ export const ClaudeDriver = {
1565
1580
  // are billed (at the cache rate) and they fill the window — but
1566
1581
  // they are reported separately too, so the UI can show how much
1567
1582
  // of the figure was context re-read rather than new text.
1568
- settle(o.is_error !== true, session.turn?.authFailed ? "auth_required" : o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
1583
+ settle(o.is_error !== true, session.turn?.authFailed
1584
+ ? "auth_required"
1585
+ : session.turn?.updateRequired
1586
+ ? "update_required"
1587
+ : o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
1569
1588
  ? {
1570
1589
  input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
1571
1590
  output: o.usage.output_tokens || 0,
@@ -6,11 +6,17 @@
6
6
  import { readdirSync, readFileSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { basename, join } from "node:path";
9
+ import { qualifiedModelLabel } from "../contracts.js";
9
10
  import { killCliTree, spawnCli } from "../procs.js";
11
+ import { codexHome } from "./codex-identity.js";
10
12
  import { mergeLocalInject } from "./local-inject.js";
11
13
  export const STATIC_CODEX_MODELS = {
12
14
  default: "gpt-5.6-sol",
13
15
  options: [
16
+ // Only used when live discovery fails; these rows do not establish account access.
17
+ { id: "gpt-6-astra", label: "GPT-6 Astra (availability unverified)" },
18
+ { id: "gpt-6-sol", label: "GPT-6 Sol (availability unverified)" },
19
+ { id: "gpt-6-luna", label: "GPT-6 Luna (availability unverified)" },
14
20
  { id: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
15
21
  { id: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
16
22
  { id: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
@@ -154,11 +160,6 @@ export function readCodexAppServerModelCatalog(cli, env, timeoutMs = 8_000) {
154
160
  request("initialize", { clientInfo: { name: "openmausbot", version: "1" } }, "initialize");
155
161
  });
156
162
  }
157
- export function codexHome(env) {
158
- if (env.CODEX_HOME)
159
- return env.CODEX_HOME;
160
- return join(env.HOME || env.USERPROFILE || homedir(), ".codex");
161
- }
162
163
  function unquote(raw) {
163
164
  let value = raw.trim();
164
165
  const hash = value.indexOf(" #");
@@ -253,7 +254,10 @@ function niceLabel(model, provider, known, named) {
253
254
  if (named.has(encoded))
254
255
  return named.get(encoded);
255
256
  const host = providerName(provider, known);
256
- return host === provider ? model : `${model} (${host})`;
257
+ return qualifiedModelLabel(model, host, {
258
+ redundantWhen: () => host === provider,
259
+ decorate: (facet) => ` (${facet})`,
260
+ });
257
261
  }
258
262
  function collectCatalogNames(home) {
259
263
  const named = new Map();
@@ -338,7 +342,11 @@ async function probeProviderModels(provider, env, fetchImpl) {
338
342
  /** Local slugs Codex already knows, plus the three official cloud rows. */
339
343
  export async function readCodexModelCatalog(env = process.env, fetchImpl = fetch, cli) {
340
344
  const official = (cli ? await readCodexAppServerModelCatalog(cli, env) : null) ?? STATIC_CODEX_MODELS;
345
+ // Match the identity boundary: an explicitly relative CODEX_HOME or user
346
+ // home must not load a catalog from a different directory.
341
347
  const home = codexHome(env);
348
+ if (!home)
349
+ return mergeLocalInject(official, env, fetchImpl);
342
350
  const mainText = readText(join(home, "config.toml"));
343
351
  if (!mainText)
344
352
  return mergeLocalInject(official, env, fetchImpl);
@@ -3,6 +3,7 @@
3
3
  // old file's email. Only the protocol's display email leaves this helper.
4
4
  import { homedir } from "node:os";
5
5
  import { isAbsolute, join, resolve } from "node:path";
6
+ import { userHome } from "../env-path.js";
6
7
  import { killCliTree, spawnCli } from "../procs.js";
7
8
  const MAX_OUTPUT = 16_384;
8
9
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -11,7 +12,7 @@ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11
12
  export function codexHome(env) {
12
13
  if (env.CODEX_HOME)
13
14
  return isAbsolute(env.CODEX_HOME) ? resolve(env.CODEX_HOME) : null;
14
- const home = env.HOME || env.USERPROFILE || homedir();
15
+ const home = userHome(env);
15
16
  return isAbsolute(home) ? join(home, ".codex") : null;
16
17
  }
17
18
  /** Read-only account metadata. Unsupported CLIs and uncertain responses stay