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
@@ -3,7 +3,11 @@
3
3
  import { Ajv } from "ajv";
4
4
  import { Ajv2020 } from "ajv/dist/2020.js";
5
5
  import formats from "ajv-formats";
6
+ import { stripControlPlaneEnv } from "../config.js";
7
+ import { augmentedPath } from "../env-path.js";
6
8
  import { killCliTree, spawnCli } from "../procs.js";
9
+ import { chatImage } from "./chat-images.js";
10
+ import { ChatBoxClient } from "./chat-box-tools.js";
7
11
  /** The transport cannot safely continue this turn. A dispatched operation may
8
12
  * already have taken effect, so callers must not retry it through a new round. */
9
13
  export class ChatToolSessionError extends Error {
@@ -21,7 +25,16 @@ function object(value) {
21
25
  return value !== null && typeof value === "object" && !Array.isArray(value);
22
26
  }
23
27
  function aborted() { return new Error("MCP operation cancelled"); }
28
+ /** These servers are a chat-runtime bot's tools, the counterpart of an engine
29
+ * CLI's children: the operator's control-plane secrets never ride along. What
30
+ * the server entry itself names is a deliberate grant and is applied last. */
31
+ export function chatMcpEnvironment(serverEnv, source = process.env) {
32
+ const env = { ...source, PATH: augmentedPath() };
33
+ stripControlPlaneEnv(env);
34
+ return { ...env, ...serverEnv };
35
+ }
24
36
  class ChatMcpClient {
37
+ frameBytes;
25
38
  child;
26
39
  buffer = "";
27
40
  nextId = 1;
@@ -29,10 +42,14 @@ class ChatMcpClient {
29
42
  closed = false;
30
43
  closing;
31
44
  pending = new Map();
32
- constructor(server) {
45
+ constructor(server, computerUse = false) {
46
+ this.frameBytes = computerUse ? 32 * 1024 * 1024 : FRAME_BYTES;
33
47
  try {
48
+ // The desktop shell inherits Finder's bare PATH, where `npx`-style
49
+ // servers cannot find `node` and exit at once. Widen it the way the
50
+ // Claude and Codex drivers do; a PATH the user set on the server wins.
34
51
  this.child = spawnCli(server.command, server.args, {
35
- stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...server.env },
52
+ stdio: ["pipe", "pipe", "pipe"], env: chatMcpEnvironment(server.env),
36
53
  });
37
54
  }
38
55
  catch {
@@ -64,7 +81,9 @@ class ChatMcpClient {
64
81
  entry.reject(new Error("MCP session closed"));
65
82
  this.pending.clear();
66
83
  this.buffer = "";
67
- this.closing = killCliTree(this.child, 500).then((stopped) => {
84
+ // Confirm within the codebase-default grace: Windows reaps the tree via
85
+ // taskkill /T, which can exceed shorter budgets on a loaded machine.
86
+ this.closing = killCliTree(this.child, 5_000).then((stopped) => {
68
87
  if (!stopped)
69
88
  throw new Error("MCP server shutdown could not be confirmed; execution outcome may be uncertain");
70
89
  });
@@ -74,7 +93,7 @@ class ChatMcpClient {
74
93
  if (this.closed || !this.child.stdin.writable || this.child.stdin.destroyed)
75
94
  throw new Error("MCP session closed");
76
95
  const encoded = JSON.stringify(frame) + "\n";
77
- if (Buffer.byteLength(encoded) > FRAME_BYTES)
96
+ if (Buffer.byteLength(encoded) > this.frameBytes)
78
97
  throw new Error("MCP request exceeds the frame limit");
79
98
  this.child.stdin.write(encoded);
80
99
  }
@@ -88,7 +107,7 @@ class ChatMcpClient {
88
107
  this.buffer = this.buffer.slice(newline + 1);
89
108
  if (++this.frames > MAX_FRAMES)
90
109
  return this.fail(new Error("MCP session exceeded the response frame count limit"));
91
- if (Buffer.byteLength(line) > FRAME_BYTES)
110
+ if (Buffer.byteLength(line) > this.frameBytes)
92
111
  return this.fail(new Error("MCP response exceeds the frame limit"));
93
112
  if (!line.trim())
94
113
  continue;
@@ -125,7 +144,7 @@ class ChatMcpClient {
125
144
  else
126
145
  entry.resolve(message.result);
127
146
  }
128
- if (Buffer.byteLength(this.buffer) > FRAME_BYTES)
147
+ if (Buffer.byteLength(this.buffer) > this.frameBytes)
129
148
  this.fail(new Error("MCP response exceeds the frame limit"));
130
149
  }
131
150
  async call(method, params, signal, timeout) {
@@ -229,6 +248,8 @@ function compileSchema(schema) {
229
248
  // ajv-formats is CommonJS and exports the plugin as both module.exports
230
249
  // and .default; the latter also matches its NodeNext declaration.
231
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 });
232
253
  try {
233
254
  return compiler.compile(schema);
234
255
  }
@@ -245,8 +266,14 @@ function boundedText(value) {
245
266
  end -= 1;
246
267
  return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
247
268
  }
248
- export async function mountChatTools(integrations, signal) {
269
+ export async function mountChatTools(integrations, signal, computerUse = false) {
249
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]);
250
277
  if (integrations?.agents)
251
278
  servers.push(["agents", integrations.agents]);
252
279
  if (integrations?.composio)
@@ -285,7 +312,9 @@ export async function mountChatTools(integrations, signal) {
285
312
  const mounts = await Promise.allSettled(servers.map(async ([name, descriptor]) => {
286
313
  if (signal.aborted || closed)
287
314
  throw aborted();
288
- 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);
289
318
  clients.push(client);
290
319
  return { name, client, tools: await client.tools(signal) };
291
320
  }));
@@ -305,6 +334,18 @@ export async function mountChatTools(integrations, signal) {
305
334
  if (Buffer.byteLength(JSON.stringify(tool.inputSchema)) > SCHEMA_BYTES)
306
335
  throw new Error("MCP tool schema exceeds the 64KB limit");
307
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) : "");
308
349
  const base = `${server}_${tool.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "mcp_tool";
309
350
  let name = base;
310
351
  for (let index = 2; registered.has(name); index += 1) {
@@ -312,7 +353,7 @@ export async function mountChatTools(integrations, signal) {
312
353
  name = base.slice(0, 64 - suffix.length) + suffix;
313
354
  }
314
355
  registered.set(name, { client, name: tool.name, schema });
315
- 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 } });
316
357
  if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
317
358
  throw new Error("MCP tool catalog exceeds the 1MB limit");
318
359
  }
@@ -349,12 +390,15 @@ export async function mountChatTools(integrations, signal) {
349
390
  if (!object(result) || !Array.isArray(result.content) || (result.isError !== undefined && typeof result.isError !== "boolean"))
350
391
  throw new Error("MCP tool returned an invalid result; execution outcome may be uncertain");
351
392
  const parts = [];
393
+ const images = [];
352
394
  let unsupported = 0;
353
395
  for (const item of result.content) {
354
396
  if (!object(item) || typeof item.type !== "string" || (item.type === "text" && typeof item.text !== "string"))
355
397
  throw new Error("MCP tool returned invalid content; execution outcome may be uncertain");
356
398
  if (item.type === "text")
357
399
  parts.push(item.text);
400
+ else if (computerUse && item.type === "image")
401
+ images.push(chatImage(item));
358
402
  else
359
403
  unsupported += 1;
360
404
  }
@@ -362,7 +406,8 @@ export async function mountChatTools(integrations, signal) {
362
406
  parts.push(JSON.stringify(result.structuredContent));
363
407
  if (unsupported)
364
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.]`);
365
- 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 } : {}) };
366
411
  }
367
412
  catch (error) {
368
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";
@@ -25,6 +25,7 @@ import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summ
25
25
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
26
26
  import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
27
27
  import { appendNative } from "./native.js";
28
+ import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
28
29
  import { SPAWNED_PROXIES } from "../proxy-paths.js";
29
30
  import { extractMcpImages } from "../mcp-tool-images.js";
30
31
  import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
@@ -322,6 +323,7 @@ export const STATIC_CLAUDE_MODELS = {
322
323
  options: [
323
324
  { id: "claude-fable-5-1", label: "Claude Fable 5.1" },
324
325
  { id: "claude-fable-5", label: "Claude Fable 5" },
326
+ { id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
325
327
  { id: "claude-opus-5", label: "Claude Opus 5" },
326
328
  { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
327
329
  { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
@@ -724,10 +726,13 @@ function decodeConfig(raw) {
724
726
  const configDir = typeof o.configDir === "string" ? o.configDir.trim() : undefined;
725
727
  if (configDir)
726
728
  resolveClaudeConfigDir(configDir);
729
+ if (o.managedModels !== undefined && (o.managed !== true || !Array.isArray(o.managedModels) || !o.managedModels.length || o.managedModels.some(model => typeof model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/.test(model))))
730
+ throw new Error("Invalid hosted Claude models.");
727
731
  return {
728
732
  cli: typeof o.cli === "string" ? o.cli : "claude",
729
733
  ...(configDir ? { configDir } : {}),
730
734
  ...(o.managed === true ? { managed: true } : {}),
735
+ ...(o.managedModels ? { managedModels: o.managedModels } : {}),
731
736
  permissionMode: mode ?? "acceptEdits",
732
737
  ...(tools !== undefined ? { tools } : {}),
733
738
  ...(disallowedTools !== undefined ? { disallowedTools } : {}),
@@ -821,7 +826,7 @@ export const ClaudeDriver = {
821
826
  if (inheritsUserConfig(catalogEnv)) {
822
827
  console.error(`claude (${instanceId}): OMB_CLAUDE_INHERIT_USER_CONFIG=1 — bots inherit this machine's Claude Code MCP servers, skills, hooks and CLAUDE.md on every turn; remove it unless a bot needs a user-scope server`);
823
828
  }
824
- let models = STATIC_CLAUDE_MODELS;
829
+ let models = config.managedModels ? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) } : STATIC_CLAUDE_MODELS;
825
830
  const refreshModels = async () => {
826
831
  if (config.managed)
827
832
  return;
@@ -932,6 +937,8 @@ export const ClaudeDriver = {
932
937
  // is a fresh sendTurn, and the attempt cap must survive across launches
933
938
  const retryState = new Map();
934
939
  const sendTurn = async (turn, logicalTurnId) => {
940
+ if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
941
+ throw new Error("This model is not assigned to this workspace.");
935
942
  if (config.managed && (!turn.model || turn.model.includes("::") || !config.configDir ||
936
943
  !input.environment.ANTHROPIC_API_KEY || !input.environment.ANTHROPIC_BASE_URL)) {
937
944
  throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
@@ -1193,6 +1200,7 @@ export const ClaudeDriver = {
1193
1200
  // parallel bot work must use the harness's durable delegate_bot path.
1194
1201
  env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
1195
1202
  const cwd = turn.cwd ?? homedir();
1203
+ const commandCwd = permissionLaunchCwd(cwd);
1196
1204
  // Everything that shapes the process, minus session/turn-specific temp
1197
1205
  // paths. Their contents are represented directly in the key instead.
1198
1206
  const privateFileFlags = new Set(["--mcp-config", "--settings"]);
@@ -1224,6 +1232,8 @@ export const ClaudeDriver = {
1224
1232
  clearTimeout(live.idleTimer);
1225
1233
  live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
1226
1234
  active.set(threadId, { stop: () => {
1235
+ if (live.turn)
1236
+ live.turn.stopRequested = true;
1227
1237
  closeSession(threadId, "interrupted");
1228
1238
  retry.cancelled = true;
1229
1239
  retryAbort.abort();
@@ -1231,12 +1241,13 @@ export const ClaudeDriver = {
1231
1241
  }, turnId, broker: live.broker });
1232
1242
  emit({ ...base(threadId, turnId), type: "turn.started" });
1233
1243
  const volatile = turn.systemVolatile ?? "";
1234
- const message = volatile === live.volatile
1244
+ const message = volatile === live.volatile && !turn.mentionTurn
1235
1245
  ? promptMsg
1236
1246
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1237
1247
  live.volatile = volatile;
1248
+ const running = live.turn;
1238
1249
  const written = await writeUser(live, threadId, message);
1239
- if (!written) {
1250
+ if (!written && !running?.stopRequested) {
1240
1251
  active.delete(threadId);
1241
1252
  live.turn = null;
1242
1253
  closeSession(threadId, "stdin write failed");
@@ -1314,6 +1325,9 @@ export const ClaudeDriver = {
1314
1325
  requestType: ask.kind,
1315
1326
  tool: ask.tool,
1316
1327
  summary: askSummary(ask),
1328
+ command: ask.kind === "permission" && ask.tool === "Bash"
1329
+ ? permissionCommand(ask.input.command, commandCwd) : undefined,
1330
+ requiresExplicitApproval: ask.kind === "permission" && ask.tool === "Bash" && ask.input.dangerouslyDisableSandbox === true || undefined,
1317
1331
  nativeReview,
1318
1332
  // the proxy hands Claude its own suggested rules on `always`;
1319
1333
  // host control stays one action at a time
@@ -1610,7 +1624,10 @@ export const ClaudeDriver = {
1610
1624
  // a turn still running when the process died is a failed turn; a
1611
1625
  // process that exited between turns (idle close, contract change)
1612
1626
  // is just a session ending
1613
- if (session.turn && !session.turn.settled) {
1627
+ if (session.turn?.stopRequested && !session.turn.settled) {
1628
+ settle(false, "interrupted");
1629
+ }
1630
+ else if (session.turn && !session.turn.settled) {
1614
1631
  // A retained process may be running a later user turn. Its close
1615
1632
  // handler must retry that request, not the process's first prompt.
1616
1633
  const { turnId, input: turn, retryAbort } = session.turn;
@@ -1794,6 +1811,8 @@ export const ClaudeDriver = {
1794
1811
  void session.finishClose();
1795
1812
  });
1796
1813
  const stop = () => {
1814
+ if (session.turn)
1815
+ session.turn.stopRequested = true;
1797
1816
  // taskkill is asynchronous on Windows. Retire steering and approvals
1798
1817
  // now, before a still-connected child can submit more work.
1799
1818
  closeSession(threadId, "interrupted");
@@ -1807,7 +1826,8 @@ export const ClaudeDriver = {
1807
1826
  // stdin stays OPEN: that is what keeps the session alive for a
1808
1827
  // mid-turn steer or the next turn; closeSession() ends it.
1809
1828
  if (!(await writeUser(session, threadId, promptMsg))) {
1810
- settle(false, "stdin_write_failed");
1829
+ if (!session.turn?.stopRequested)
1830
+ settle(false, "stdin_write_failed");
1811
1831
  closeSession(threadId, "stdin write failed");
1812
1832
  }
1813
1833
  return { turnId };
@@ -1844,9 +1864,9 @@ export const ClaudeDriver = {
1844
1864
  * `claude -p "prompt"` shape is not safe for review. No tools or MCP
1845
1865
  * servers are mounted in this isolated process. */
1846
1866
  const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
1847
- const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
1867
+ const child = spawnCli(config.cli, ["-p", "--model", config.managedModels?.[0] ?? "claude-haiku-4-5", "--output-format", "text"], {
1848
1868
  stdio: ["pipe", "pipe", "pipe"],
1849
- env: environment("claude-haiku-4-5"),
1869
+ env: environment(config.managedModels?.[0] ?? "claude-haiku-4-5"),
1850
1870
  });
1851
1871
  let stdout = "";
1852
1872
  let stderr = "";
@@ -1,10 +1,35 @@
1
- // Codex owns history; this receipt only remembers which bot rules it accepted.
1
+ // Codex owns history; this receipt only remembers which bot rules and which
2
+ // volatile context digest the native thread last carried.
2
3
  import { createHash } from "node:crypto";
3
4
  import { mkdirSync, readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
5
6
  import { DATA_DIR } from "../config.js";
6
7
  import { writeFileAtomic } from "../atomic.js";
7
8
  const digest = (value) => createHash("sha256").update(value).digest("hex");
9
+ const isFingerprint = (value) => /^[0-9a-f]{64}$/.test(value);
10
+ function parseReceipt(raw) {
11
+ if (!raw)
12
+ return null;
13
+ const trimmed = raw.trim();
14
+ if (isFingerprint(trimmed))
15
+ return { instructions: trimmed };
16
+ try {
17
+ const parsed = JSON.parse(trimmed);
18
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19
+ const record = parsed;
20
+ if (typeof record.instructions === "string" && isFingerprint(record.instructions)) {
21
+ return {
22
+ instructions: record.instructions,
23
+ ...(typeof record.volatile === "string" && isFingerprint(record.volatile) ? { volatile: record.volatile } : {}),
24
+ };
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ /* a corrupt receipt is simply unknown */
30
+ }
31
+ return null;
32
+ }
8
33
  /** Preserve native configured rules when overriding Codex's developer slot. */
9
34
  export function codexDeveloperInstructions(config, botInstructions) {
10
35
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -20,19 +45,21 @@ export function codexDeveloperInstructions(config, botInstructions) {
20
45
  ? `${botInstructions || "No OpenMausBot bot-specific instructions remain."}\n\n${configured}`
21
46
  : botInstructions;
22
47
  }
23
- export async function syncCodexInstructions(key, nativeThreadId, instructions, resumed, request) {
48
+ export async function syncCodexInstructions(key, nativeThreadId, instructions, volatile, resumed, request, mentionTurn = false) {
24
49
  const directory = join(DATA_DIR, "codex-instructions");
25
50
  const path = join(directory, `${digest(JSON.stringify([key, nativeThreadId]))}.sha256`);
26
51
  const fingerprint = digest(instructions);
52
+ const volatileFingerprint = digest(volatile);
27
53
  let previous;
28
54
  try {
29
- previous = readFileSync(path, "utf8");
55
+ previous = parseReceipt(readFileSync(path, "utf8"));
30
56
  }
31
57
  catch (error) {
32
58
  if (error.code !== "ENOENT")
33
59
  throw error;
60
+ previous = null;
34
61
  }
35
- if (resumed && previous !== fingerprint) {
62
+ if (resumed && previous?.instructions !== fingerprint) {
36
63
  // thread/resume config overrides are used after compaction, but Codex
37
64
  // 0.153.4 keeps the old initial developer message until then. Append a
38
65
  // developer update ONLY on change (or first adoption of an old session).
@@ -59,8 +86,23 @@ export async function syncCodexInstructions(key, nativeThreadId, instructions, r
59
86
  throw error;
60
87
  }
61
88
  }
62
- if (!resumed || previous !== fingerprint) {
89
+ // A mention describes the turn it rides: deliver it on every tagged turn,
90
+ // even when the volatile text is byte-identical to the previous turn's.
91
+ const deliverVolatile = !resumed || previous?.volatile !== volatileFingerprint || mentionTurn;
92
+ const hadVolatile = typeof previous?.volatile === "string" && previous.volatile !== digest("");
93
+ if (!resumed || previous?.instructions !== fingerprint) {
94
+ // Instructions-only, and only after inject_items acknowledged: the
95
+ // volatile digest stays pending until the driver commits it after the
96
+ // provider accepts the turn, so a submission the provider rejected
97
+ // redelivers its context on the next attempt.
63
98
  mkdirSync(directory, { recursive: true });
64
- writeFileAtomic(path, fingerprint, { mode: 0o600 });
99
+ writeFileAtomic(path, JSON.stringify({ instructions: fingerprint }), { mode: 0o600 });
65
100
  }
101
+ const commitVolatile = previous?.instructions === fingerprint && previous?.volatile === volatileFingerprint
102
+ ? null
103
+ : () => {
104
+ mkdirSync(directory, { recursive: true });
105
+ writeFileAtomic(path, JSON.stringify({ instructions: fingerprint, volatile: volatileFingerprint }), { mode: 0o600 });
106
+ };
107
+ return { deliverVolatile, hadVolatile, commitVolatile };
66
108
  }
@@ -0,0 +1,44 @@
1
+ // MCP server names the user already declared in their own Codex config.
2
+ //
3
+ // Custom MCP servers reach codex as `-c mcp_servers.<name>.…` overrides, and
4
+ // codex merges an override into any same-named table in config.toml. A stdio
5
+ // command laid over a remote `url` entry is "invalid configuration": codex
6
+ // falls back to defaults, config/read fails, and the whole turn dies before
7
+ // the model is even asked. Mounting a colliding server under a name of its
8
+ // own keeps both definitions usable.
9
+ import { readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ const HEADER = /^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([A-Za-z0-9_-]+))[ \t]*[.\]]/gm;
13
+ /** Names declared as `[mcp_servers.<name>]` (or a sub-table of one). Inline
14
+ * tables are not scanned; a collision there still surfaces as codex's own
15
+ * configuration error. */
16
+ export function mcpServerNamesInToml(toml) {
17
+ const names = new Set();
18
+ for (const match of toml.matchAll(HEADER)) {
19
+ const name = match[1] !== undefined ? match[1].replace(/\\(.)/g, "$1") : match[2] ?? match[3];
20
+ if (name)
21
+ names.add(name);
22
+ }
23
+ return names;
24
+ }
25
+ /** Server names in the config.toml of the Codex home this child will use. */
26
+ export function codexConfigMcpServerNames(env) {
27
+ const codexHome = env.CODEX_HOME || join(env.HOME || env.USERPROFILE || homedir(), ".codex");
28
+ try {
29
+ return mcpServerNamesInToml(readFileSync(join(codexHome, "config.toml"), "utf8"));
30
+ }
31
+ catch {
32
+ return new Set();
33
+ }
34
+ }
35
+ /** The name to mount a custom server under: its own, unless the user's Codex
36
+ * config already has a server by that name. */
37
+ export function mountedMcpServerName(name, taken) {
38
+ if (!taken.has(name))
39
+ return name;
40
+ let candidate = `${name}_openmausbot`;
41
+ for (let i = 2; taken.has(candidate); i++)
42
+ candidate = `${name}_openmausbot${i}`;
43
+ return candidate;
44
+ }
@@ -0,0 +1,20 @@
1
+ // OpenMausBot owns computer selection and mounts its scoped MCP servers.
2
+ // Keep the standalone Codex installation untouched: these overrides apply
3
+ // only to the child process, including resumed conversations and retries.
4
+ // The desktop-app browser is not available in Codex CLI. Enabling its tool
5
+ // without a desktop connection exposes an unusable route; native search is
6
+ // independent and must remain governed by the user's/provider's config.
7
+ // https://developers.openai.com/codex/browser
8
+ export function codexToolSurfaceArgs() {
9
+ return [
10
+ "-c", "features.browser_use=false",
11
+ "-c", "features.browser_use_external=false",
12
+ "-c", "features.computer_use=false",
13
+ // Disable the matching instructions as well as the native tool. Use an
14
+ // inline table: quoted dotted keys are interpreted literally by some CLIs.
15
+ "-c", 'plugins={ "browser@openai-bundled" = { enabled = false }, "computer-use@openai-bundled" = { enabled = false }, "unified-computer-use@openai-bundled" = { enabled = false } }',
16
+ // Native web search does not require the desktop browser connection.
17
+ // Preserve the user's/provider's search mode instead of disabling it for
18
+ // every OpenMausBot process after a failure in one search route.
19
+ ];
20
+ }