openmausbot 0.1.84 → 0.1.86

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 (230) hide show
  1. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  2. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  4. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  10. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  16. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  19. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  28. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  31. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  34. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  43. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  46. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  49. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  52. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  54. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  57. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  60. package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
  61. package/dist/assets/arc-DW7mrI4p.js +1 -0
  62. package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
  63. package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
  64. package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
  65. package/dist/assets/channel-BXFkgus9.js +1 -0
  66. package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
  67. package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
  68. package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
  69. package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
  70. package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
  71. package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
  72. package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
  73. package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
  74. package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
  75. package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
  76. package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
  77. package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
  78. package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
  79. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  80. package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
  81. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  82. package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
  83. package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
  84. package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
  85. package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
  86. package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
  87. package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
  88. package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
  89. package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
  90. package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
  91. package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
  92. package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
  93. package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
  94. package/dist/assets/graph-D8UmR1vK.js +1 -0
  95. package/dist/assets/index-BGgqlWPJ.css +1 -0
  96. package/dist/assets/index-BtHo8ZKl.js +577 -0
  97. package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
  98. package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
  99. package/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
  101. package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
  102. package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
  103. package/dist/assets/linear-3su2TG93.js +1 -0
  104. package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
  105. package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
  106. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  107. package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
  108. package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
  109. package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
  110. package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
  111. package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
  112. package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
  113. package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
  114. package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
  115. package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
  116. package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
  117. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
  118. package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
  119. package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
  120. package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
  121. package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
  122. package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
  123. package/dist/index.html +2 -2
  124. package/dist-server/companion/src/routes.js +1 -0
  125. package/dist-server/container-mcp.js +231 -17
  126. package/dist-server/drivers/agents-proxy.js +16097 -1411
  127. package/dist-server/enterprise/LICENSE +32 -0
  128. package/dist-server/enterprise/server/index.js +6 -5
  129. package/dist-server/hooks/omb-hook.js +74 -0
  130. package/dist-server/index.js +12495 -7584
  131. package/dist-server/local-computer-proxy.js +83 -1
  132. package/dist-server/local-computer.js +14 -2
  133. package/dist-server/mcp-server.js +3 -1
  134. package/dist-server/openmausbot.js +5458 -4587
  135. package/dist-server/pair-cli.js +5458 -4587
  136. package/dist-server/proxy-paths.js +1 -0
  137. package/dist-server/server/admin-activity.js +410 -0
  138. package/dist-server/server/agent-tool-policy.js +1 -0
  139. package/dist-server/server/bot-package.js +2 -0
  140. package/dist-server/server/bot-visibility.js +462 -0
  141. package/dist-server/server/box.js +25 -5
  142. package/dist-server/server/browser-engine.js +87 -10
  143. package/dist-server/server/browser-live.js +7 -5
  144. package/dist-server/server/browser-runtime.js +66 -11
  145. package/dist-server/server/channel-queue.js +2 -0
  146. package/dist-server/server/checkpoints.js +62 -18
  147. package/dist-server/server/cli-prompts.js +26 -2
  148. package/dist-server/server/cli.js +79 -10
  149. package/dist-server/server/commands.js +27 -0
  150. package/dist-server/server/compaction-summary.js +78 -0
  151. package/dist-server/server/composio.js +49 -11
  152. package/dist-server/server/config.js +137 -14
  153. package/dist-server/server/context-budget.js +29 -0
  154. package/dist-server/server/context-rebuild.js +72 -0
  155. package/dist-server/server/decision-log.js +209 -48
  156. package/dist-server/server/default-model-selection.js +20 -3
  157. package/dist-server/server/delegations.js +11 -9
  158. package/dist-server/server/digest.js +130 -0
  159. package/dist-server/server/drivers/acp/core.js +637 -238
  160. package/dist-server/server/drivers/acp/grok.js +4 -3
  161. package/dist-server/server/drivers/agents-call.js +1123 -0
  162. package/dist-server/server/drivers/agents-catalog.js +659 -0
  163. package/dist-server/server/drivers/agents-client.js +28 -0
  164. package/dist-server/server/drivers/agents-proxy.js +18 -1683
  165. package/dist-server/server/drivers/agents-result.js +23 -0
  166. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  167. package/dist-server/server/drivers/claude.js +83 -16
  168. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  169. package/dist-server/server/drivers/codex.js +32 -4
  170. package/dist-server/server/drivers/grok.js +2 -1
  171. package/dist-server/server/drivers/openai-chat.js +66 -45
  172. package/dist-server/server/drivers/openai-compat.js +16 -8
  173. package/dist-server/server/enterprise.js +97 -16
  174. package/dist-server/server/external-runtime.js +83 -0
  175. package/dist-server/server/guarded-requests.js +60 -0
  176. package/dist-server/server/harness/http.js +61 -0
  177. package/dist-server/server/hooks/omb-hook.js +103 -0
  178. package/dist-server/server/hosted-models.js +151 -0
  179. package/dist-server/server/hosted-slack.js +11 -0
  180. package/dist-server/server/index.js +2810 -678
  181. package/dist-server/server/local-computer.js +21 -2
  182. package/dist-server/server/managed-desktop.js +79 -14
  183. package/dist-server/server/managed-policy.js +153 -0
  184. package/dist-server/server/mcp-bridge.js +10 -2
  185. package/dist-server/server/mcp-tool-images.js +37 -0
  186. package/dist-server/server/mcp-tool-schema.js +108 -0
  187. package/dist-server/server/message-db.js +108 -21
  188. package/dist-server/server/message-file.js +3 -2
  189. package/dist-server/server/model-context-window.js +7 -0
  190. package/dist-server/server/model-prices.js +121 -0
  191. package/dist-server/server/notify.js +10 -1
  192. package/dist-server/server/package-export.js +1 -0
  193. package/dist-server/server/peer-roster.js +17 -4
  194. package/dist-server/server/provider-icon.js +20 -0
  195. package/dist-server/server/proxy-paths.js +1 -0
  196. package/dist-server/server/recent-work.js +15 -0
  197. package/dist-server/server/request-auth.js +97 -0
  198. package/dist-server/server/room-handoffs.js +41 -13
  199. package/dist-server/server/routes/hosted-slack.js +20 -0
  200. package/dist-server/server/routes/table.js +14 -0
  201. package/dist-server/server/routine-requests.js +39 -1
  202. package/dist-server/server/routines.js +65 -9
  203. package/dist-server/server/shared-computer-control.js +31 -4
  204. package/dist-server/server/spend.js +111 -16
  205. package/dist-server/server/steer-queue.js +12 -1
  206. package/dist-server/server/store.js +384 -33
  207. package/dist-server/server/system-prompt.js +4 -4
  208. package/dist-server/server/team-backup.js +19 -2
  209. package/dist-server/server/thread-starters.js +53 -0
  210. package/dist-server/server/tool-results.js +72 -0
  211. package/dist-server/server/tts/grok.js +74 -0
  212. package/dist-server/server/tts/index.js +19 -1
  213. package/dist-server/server/usage-ledger.js +31 -8
  214. package/dist-server/server/webhooks.js +28 -0
  215. package/dist-server/server/workspace-backup-policy.js +9 -2
  216. package/dist-server/server/workspace-backup.js +7 -4
  217. package/dist-server/shared/approval-mode.js +7 -1
  218. package/dist-server/shared/digest.js +1 -0
  219. package/dist-server/shared/markdown-windows-paths.js +36 -0
  220. package/dist-server/shared/mention-boundary.js +15 -0
  221. package/dist-server/shared/provider-icon.js +110 -0
  222. package/dist-server/shared/routines.js +7 -1
  223. package/dist-server/shared/team-backup.js +5 -0
  224. package/dist-server/src/lib/composer-attachments.js +16 -0
  225. package/dist-server/vps-container-mcp.js +236 -22
  226. package/enterprise/README.md +33 -12
  227. package/enterprise/server/index.js +6 -5
  228. package/package.json +1 -1
  229. package/dist/assets/index-BbU5REzd.js +0 -310
  230. package/dist/assets/index-CLGfYlx_.css +0 -1
@@ -0,0 +1,23 @@
1
+ import { redactSecretsInText } from "../../shared/redact.js";
2
+ import { TOOL_RESULT_MAX_CHARS, TOOL_RESULT_PREVIEW_CHARS, toolResultPrefix } from "../tool-results.js";
3
+ /** The operation already happened. Saving overflow must never retry it or
4
+ * turn a successful operation into a failed MCP call. Only cache I/O is timed. */
5
+ export async function boundedAgentResult(text, save) {
6
+ if (text.length <= 24_000)
7
+ return text;
8
+ const redacted = redactSecretsInText(text);
9
+ const prefix = toolResultPrefix(redacted, TOOL_RESULT_PREVIEW_CHARS);
10
+ const retained = toolResultPrefix(redacted, TOOL_RESULT_MAX_CHARS);
11
+ const truncated = retained.length < redacted.length;
12
+ try {
13
+ const saved = await save(retained, truncated);
14
+ if (!saved || typeof saved.id !== "string" || !/^r-[0-9a-f-]{36}$/.test(saved.id))
15
+ throw new Error("Invalid saved result");
16
+ return `${prefix}\n\n[Large tool result: showing the first ${prefix.length} characters. ${truncated || saved.truncated
17
+ ? "Only a bounded portion was retained; the remaining tail was omitted."
18
+ : "The remaining redacted result is temporarily saved."} If a missing detail is needed, call tool_result_read with id "${saved.id}" and offset ${prefix.length}. Saved results expire after one hour, on app restart, or under cache pressure. Do not repeat an action just to retrieve its output.]`;
19
+ }
20
+ catch {
21
+ return `${prefix}\n\n[Large tool result: showing the first ${prefix.length} characters. The remaining output could not be saved. The original operation was not retried. Do not repeat an action just to retrieve its output.]`;
22
+ }
23
+ }
@@ -3,6 +3,8 @@
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";
7
9
  /** The transport cannot safely continue this turn. A dispatched operation may
8
10
  * already have taken effect, so callers must not retry it through a new round. */
@@ -21,6 +23,14 @@ function object(value) {
21
23
  return value !== null && typeof value === "object" && !Array.isArray(value);
22
24
  }
23
25
  function aborted() { return new Error("MCP operation cancelled"); }
26
+ /** These servers are a chat-runtime bot's tools, the counterpart of an engine
27
+ * CLI's children: the operator's control-plane secrets never ride along. What
28
+ * the server entry itself names is a deliberate grant and is applied last. */
29
+ export function chatMcpEnvironment(serverEnv, source = process.env) {
30
+ const env = { ...source, PATH: augmentedPath() };
31
+ stripControlPlaneEnv(env);
32
+ return { ...env, ...serverEnv };
33
+ }
24
34
  class ChatMcpClient {
25
35
  child;
26
36
  buffer = "";
@@ -31,8 +41,11 @@ class ChatMcpClient {
31
41
  pending = new Map();
32
42
  constructor(server) {
33
43
  try {
44
+ // The desktop shell inherits Finder's bare PATH, where `npx`-style
45
+ // servers cannot find `node` and exit at once. Widen it the way the
46
+ // Claude and Codex drivers do; a PATH the user set on the server wins.
34
47
  this.child = spawnCli(server.command, server.args, {
35
- stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...server.env },
48
+ stdio: ["pipe", "pipe", "pipe"], env: chatMcpEnvironment(server.env),
36
49
  });
37
50
  }
38
51
  catch {
@@ -64,7 +77,9 @@ class ChatMcpClient {
64
77
  entry.reject(new Error("MCP session closed"));
65
78
  this.pending.clear();
66
79
  this.buffer = "";
67
- this.closing = killCliTree(this.child, 500).then((stopped) => {
80
+ // Confirm within the codebase-default grace: Windows reaps the tree via
81
+ // taskkill /T, which can exceed shorter budgets on a loaded machine.
82
+ this.closing = killCliTree(this.child, 5_000).then((stopped) => {
68
83
  if (!stopped)
69
84
  throw new Error("MCP server shutdown could not be confirmed; execution outcome may be uncertain");
70
85
  });
@@ -9,11 +9,12 @@
9
9
  // - the bot's cloud computer (box.ascii.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
- import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import { createServer as createNetServer } from "node:net";
14
14
  import { homedir, tmpdir } from "node:os";
15
15
  import { join, dirname, isAbsolute, normalize } from "node:path";
16
16
  import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
17
+ import { writeFileAtomic } from "../atomic.js";
17
18
  import { augmentedPath } from "../env-path.js";
18
19
  import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
19
20
  import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
@@ -25,6 +26,7 @@ import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS }
25
26
  import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
26
27
  import { appendNative } from "./native.js";
27
28
  import { SPAWNED_PROXIES } from "../proxy-paths.js";
29
+ import { extractMcpImages } from "../mcp-tool-images.js";
28
30
  import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
29
31
  /** Whether `claude` has been signed in.
30
32
  *
@@ -320,6 +322,7 @@ export const STATIC_CLAUDE_MODELS = {
320
322
  options: [
321
323
  { id: "claude-fable-5-1", label: "Claude Fable 5.1" },
322
324
  { id: "claude-fable-5", label: "Claude Fable 5" },
325
+ { id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
323
326
  { id: "claude-opus-5", label: "Claude Opus 5" },
324
327
  { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
325
328
  { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
@@ -392,6 +395,7 @@ export function readClaudeModelCatalog(env = process.env) {
392
395
  // far. See server/proxy-paths.ts.
393
396
  const PERM_PROXY_PATH = SPAWNED_PROXIES.permission;
394
397
  const DWEB_PROXY_PATH = SPAWNED_PROXIES.dweb;
398
+ const HOOK_HELPER_PATH = SPAWNED_PROXIES.hook;
395
399
  // in the packaged app process.execPath is the Electron binary — this env
396
400
  // makes it behave as plain node for the spawned MCP proxies (harmless in dev)
397
401
  const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
@@ -430,6 +434,26 @@ function askSummary(ask) {
430
434
  return askQuestionSummary(questions).slice(0, 300);
431
435
  return askInputSummary(ask.input) ?? ask.tool ?? "tool";
432
436
  }
437
+ /** Where the hook helper reads this thread's current turn token. Stable per
438
+ * thread (so the CLI's environment can name it once) and private. */
439
+ export function hookTokenFile(threadId, botId) {
440
+ const digest = createHash("sha256").update(`${botId ?? ""}\0${threadId}`).digest("hex").slice(0, 24);
441
+ return join(DATA_DIR, "hook-tokens", `${digest}.token`);
442
+ }
443
+ /** The `hooks` block for the private --settings file: one command for each
444
+ * event the harness observes. Claude Code runs it with the event JSON on
445
+ * stdin and applies any hookSpecificOutput it prints. The command string is
446
+ * a shell line, so both paths are quoted (this repo's own path has a space). */
447
+ export function claudeHookSettings(helperPath) {
448
+ // JSON quoting is not shell quoting: $(), backticks and $names still
449
+ // expand inside double quotes on POSIX. Windows paths come through env
450
+ // variables so their backslashes are not JSON-escaped into the command.
451
+ const command = process.platform === "win32"
452
+ ? '"%OMB_HOOK_NODE%" "%OMB_HOOK_HELPER%"'
453
+ : [process.execPath, helperPath].map(path => `'${path.replace(/'/g, "'\\''")}'`).join(" ");
454
+ const entry = [{ matcher: "", hooks: [{ type: "command", command, timeout: 5 }] }];
455
+ return { PostToolUse: entry, PreCompact: entry, SessionStart: entry, Stop: entry };
456
+ }
433
457
  export function permissionSocketPath(threadId, botId) {
434
458
  // A readable prefix alone is not unique: ids that agree on their first
435
459
  // characters ("t-perm-dup-1", "t-perm-dup-2") would share a socket. POSIX
@@ -701,10 +725,13 @@ function decodeConfig(raw) {
701
725
  const configDir = typeof o.configDir === "string" ? o.configDir.trim() : undefined;
702
726
  if (configDir)
703
727
  resolveClaudeConfigDir(configDir);
728
+ 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))))
729
+ throw new Error("Invalid hosted Claude models.");
704
730
  return {
705
731
  cli: typeof o.cli === "string" ? o.cli : "claude",
706
732
  ...(configDir ? { configDir } : {}),
707
733
  ...(o.managed === true ? { managed: true } : {}),
734
+ ...(o.managedModels ? { managedModels: o.managedModels } : {}),
708
735
  permissionMode: mode ?? "acceptEdits",
709
736
  ...(tools !== undefined ? { tools } : {}),
710
737
  ...(disallowedTools !== undefined ? { disallowedTools } : {}),
@@ -798,7 +825,7 @@ export const ClaudeDriver = {
798
825
  if (inheritsUserConfig(catalogEnv)) {
799
826
  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`);
800
827
  }
801
- let models = STATIC_CLAUDE_MODELS;
828
+ let models = config.managedModels ? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) } : STATIC_CLAUDE_MODELS;
802
829
  const refreshModels = async () => {
803
830
  if (config.managed)
804
831
  return;
@@ -909,6 +936,8 @@ export const ClaudeDriver = {
909
936
  // is a fresh sendTurn, and the attempt cap must survive across launches
910
937
  const retryState = new Map();
911
938
  const sendTurn = async (turn, logicalTurnId) => {
939
+ if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
940
+ throw new Error("This model is not assigned to this workspace.");
912
941
  if (config.managed && (!turn.model || turn.model.includes("::") || !config.configDir ||
913
942
  !input.environment.ANTHROPIC_API_KEY || !input.environment.ANTHROPIC_BASE_URL)) {
914
943
  throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
@@ -953,7 +982,7 @@ export const ClaudeDriver = {
953
982
  // a retry relaunches the whole CLI; the backoff is scaled down in tests
954
983
  // so a fake's transient failures don't stall real seconds
955
984
  const retryScale = Number(process.env.FAKE_CLAUDE_RETRY_SCALE ?? "1");
956
- const sessionId = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
985
+ const sessionId = !turn.sessionReset && typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
957
986
  const newSessionId = sessionId ? null : newId();
958
987
  const args = [
959
988
  "-p",
@@ -1140,7 +1169,28 @@ export const ClaudeDriver = {
1140
1169
  const env = environment(turnModel);
1141
1170
  const authSettings = isolated && !injected.injected
1142
1171
  ? readClaudeAuthSettings(env, input.environment) : {};
1143
- const authSettingsPath = mcpConfigPath && Object.keys(authSettings).length
1172
+ // Harness hooks (item 0.2): one helper command for the events the
1173
+ // harness observes. The helper reads its bearer from a per-thread file
1174
+ // the harness rewrites every turn, so a long-lived CLI process never
1175
+ // presents a stale token. Registered through the same private
1176
+ // --settings file as the auth override; both are 0600 and per launch.
1177
+ const hooks = turn.integrations?.hooks;
1178
+ const hookTokenPath = hooks ? hookTokenFile(threadId, botId) : null;
1179
+ if (hooks && hookTokenPath) {
1180
+ mkdirSync(dirname(hookTokenPath), { recursive: true, mode: 0o700 });
1181
+ writeFileAtomic(hookTokenPath, hooks.token, { mode: 0o600 });
1182
+ env.OMB_HOOK_URL = hooks.url;
1183
+ env.OMB_HOOK_TOKEN_FILE = hookTokenPath;
1184
+ env.OMB_HOOK_NODE = process.execPath;
1185
+ env.OMB_HOOK_HELPER = HOOK_HELPER_PATH;
1186
+ // in the packaged app process.execPath is Electron — run the helper as node
1187
+ if (process.versions.electron)
1188
+ env.ELECTRON_RUN_AS_NODE = "1";
1189
+ }
1190
+ const settings = { ...authSettings };
1191
+ if (hooks)
1192
+ settings.hooks = claudeHookSettings(HOOK_HELPER_PATH);
1193
+ const authSettingsPath = mcpConfigPath && Object.keys(settings).length
1144
1194
  ? join(dirname(mcpConfigPath), "auth-settings.json") : null;
1145
1195
  if (authSettingsPath)
1146
1196
  args.push("--settings", authSettingsPath);
@@ -1163,6 +1213,8 @@ export const ClaudeDriver = {
1163
1213
  model: injected.model ?? null,
1164
1214
  base: env.ANTHROPIC_BASE_URL ?? null,
1165
1215
  configDir: env.CLAUDE_CONFIG_DIR ?? null,
1216
+ // hooks on/off changes the settings file the process was launched with
1217
+ hooks: Boolean(hooks),
1166
1218
  // Rotating an account's key/helper must not reuse the old process.
1167
1219
  auth: createHash("sha256").update(JSON.stringify({
1168
1220
  settings: authSettings,
@@ -1170,14 +1222,16 @@ export const ClaudeDriver = {
1170
1222
  })).digest("hex"),
1171
1223
  });
1172
1224
  // Reuse the live process when it is idle, unchanged, and is the session
1173
- // the harness wants resumed. Anything else: close it and spawn fresh
1174
- // (with --resume, so the conversation continues in the new process).
1225
+ // the harness wants resumed. Clearing a cursor alone does not opt out
1226
+ // of legacy reuse: an explicit rebuild must discard the idle context.
1175
1227
  const live = sessions.get(threadId);
1176
- if (live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
1228
+ if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
1177
1229
  if (live.idleTimer)
1178
1230
  clearTimeout(live.idleTimer);
1179
1231
  live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
1180
1232
  active.set(threadId, { stop: () => {
1233
+ if (live.turn)
1234
+ live.turn.stopRequested = true;
1181
1235
  closeSession(threadId, "interrupted");
1182
1236
  retry.cancelled = true;
1183
1237
  retryAbort.abort();
@@ -1189,8 +1243,9 @@ export const ClaudeDriver = {
1189
1243
  ? promptMsg
1190
1244
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1191
1245
  live.volatile = volatile;
1246
+ const running = live.turn;
1192
1247
  const written = await writeUser(live, threadId, message);
1193
- if (!written) {
1248
+ if (!written && !running?.stopRequested) {
1194
1249
  active.delete(threadId);
1195
1250
  live.turn = null;
1196
1251
  closeSession(threadId, "stdin write failed");
@@ -1213,7 +1268,7 @@ export const ClaudeDriver = {
1213
1268
  return { turnId };
1214
1269
  }
1215
1270
  if (live)
1216
- closeSession(threadId, "spawn contract changed");
1271
+ closeSession(threadId, turn.sessionReset ? "context reset" : "spawn contract changed");
1217
1272
  // Until sessions.set() below, this turn owns every launch resource.
1218
1273
  // Any bind, private-config or synchronous spawn failure must release
1219
1274
  // them here rather than leave a live listener or credential temp file.
@@ -1308,7 +1363,7 @@ export const ClaudeDriver = {
1308
1363
  writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
1309
1364
  }
1310
1365
  if (authSettingsPath) {
1311
- writeFileSync(authSettingsPath, JSON.stringify(authSettings), { mode: 0o600 });
1366
+ writeFileSync(authSettingsPath, JSON.stringify(settings), { mode: 0o600 });
1312
1367
  }
1313
1368
  if (sessionId)
1314
1369
  args.push("--resume", sessionId);
@@ -1493,6 +1548,9 @@ export const ClaudeDriver = {
1493
1548
  for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
1494
1549
  if (b.type === "tool_result") {
1495
1550
  emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error, output: toolDetailPreview(b.content) });
1551
+ for (const img of extractMcpImages(b.content)) {
1552
+ emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "assistant_image", data: img.data });
1553
+ }
1496
1554
  }
1497
1555
  }
1498
1556
  break;
@@ -1561,7 +1619,10 @@ export const ClaudeDriver = {
1561
1619
  // a turn still running when the process died is a failed turn; a
1562
1620
  // process that exited between turns (idle close, contract change)
1563
1621
  // is just a session ending
1564
- if (session.turn && !session.turn.settled) {
1622
+ if (session.turn?.stopRequested && !session.turn.settled) {
1623
+ settle(false, "interrupted");
1624
+ }
1625
+ else if (session.turn && !session.turn.settled) {
1565
1626
  // A retained process may be running a later user turn. Its close
1566
1627
  // handler must retry that request, not the process's first prompt.
1567
1628
  const { turnId, input: turn, retryAbort } = session.turn;
@@ -1627,7 +1688,9 @@ export const ClaudeDriver = {
1627
1688
  active.set(threadId, { stop: () => { retry.cancelled = true; retryAbort.abort(); }, turnId });
1628
1689
  try {
1629
1690
  const cursor = session.sessionId ?? sessionId ?? undefined;
1630
- await sendTurn({ ...turn, resumeCursor: cursor }, turnId);
1691
+ // The reset was consumed by the initial launch. Retry the
1692
+ // new session, never the context that launch replaced.
1693
+ await sendTurn({ ...turn, sessionReset: false, resumeCursor: cursor }, turnId);
1631
1694
  }
1632
1695
  catch (e) {
1633
1696
  if (active.get(threadId)?.turnId === turnId)
@@ -1743,6 +1806,8 @@ export const ClaudeDriver = {
1743
1806
  void session.finishClose();
1744
1807
  });
1745
1808
  const stop = () => {
1809
+ if (session.turn)
1810
+ session.turn.stopRequested = true;
1746
1811
  // taskkill is asynchronous on Windows. Retire steering and approvals
1747
1812
  // now, before a still-connected child can submit more work.
1748
1813
  closeSession(threadId, "interrupted");
@@ -1756,7 +1821,8 @@ export const ClaudeDriver = {
1756
1821
  // stdin stays OPEN: that is what keeps the session alive for a
1757
1822
  // mid-turn steer or the next turn; closeSession() ends it.
1758
1823
  if (!(await writeUser(session, threadId, promptMsg))) {
1759
- settle(false, "stdin_write_failed");
1824
+ if (!session.turn?.stopRequested)
1825
+ settle(false, "stdin_write_failed");
1760
1826
  closeSession(threadId, "stdin write failed");
1761
1827
  }
1762
1828
  return { turnId };
@@ -1793,9 +1859,9 @@ export const ClaudeDriver = {
1793
1859
  * `claude -p "prompt"` shape is not safe for review. No tools or MCP
1794
1860
  * servers are mounted in this isolated process. */
1795
1861
  const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
1796
- const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
1862
+ const child = spawnCli(config.cli, ["-p", "--model", config.managedModels?.[0] ?? "claude-haiku-4-5", "--output-format", "text"], {
1797
1863
  stdio: ["pipe", "pipe", "pipe"],
1798
- env: environment("claude-haiku-4-5"),
1864
+ env: environment(config.managedModels?.[0] ?? "claude-haiku-4-5"),
1799
1865
  });
1800
1866
  let stdout = "";
1801
1867
  let stderr = "";
@@ -1887,6 +1953,7 @@ export const ClaudeDriver = {
1887
1953
  // Harness turns reassert a per-bot mode and restore the broker even
1888
1954
  // when an old instance was configured with bypassPermissions.
1889
1955
  localComputerMcp: true,
1956
+ hooks: true,
1890
1957
  },
1891
1958
  sendTurn,
1892
1959
  steer,
@@ -1914,7 +1981,7 @@ export const ClaudeDriver = {
1914
1981
  return () => listeners.delete(listener);
1915
1982
  },
1916
1983
  },
1917
- generateText: (prompt) => generateReview(prompt),
1984
+ generateText: (prompt, options) => generateReview(prompt, options?.signal),
1918
1985
  reviewPermission: generateReview,
1919
1986
  dispose: async () => {
1920
1987
  try {
@@ -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
+ }
@@ -11,6 +11,7 @@
11
11
  // and preserves that history or reports a failed resume.
12
12
  import { existsSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
+ import { codexConfigMcpServerNames, mountedMcpServerName } from "./codex-mcp-names.js";
14
15
  import { stripWorkspaceCredentialEnv } from "../config.js";
15
16
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
16
17
  import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
@@ -25,6 +26,7 @@ import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instr
25
26
  import { CodexDeviceAuthController } from "./codex-device-auth.js";
26
27
  import { codexAccountEmail } from "./codex-identity.js";
27
28
  import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
29
+ import { extractMcpImages } from "../mcp-tool-images.js";
28
30
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
29
31
  const DRIVER_KIND = "codex";
30
32
  const ASTRA_MODEL_ID = "gpt-6-astra";
@@ -127,6 +129,14 @@ export function managedCodexArgs(config) {
127
129
  const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
128
130
  const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
129
131
  const skippedSseServers = new Set();
132
+ const renamedMcpServers = new Set();
133
+ /** Logged once per name: the rename is deliberate, not a lost server. */
134
+ function noteRenamedMcpServer(name, mountName) {
135
+ if (renamedMcpServers.has(name))
136
+ return;
137
+ renamedMcpServers.add(name);
138
+ console.error(`codex: MCP server ${JSON.stringify(name)} is also declared in Codex's own config.toml — mounted as ${JSON.stringify(mountName)} for this bot so the two do not merge`);
139
+ }
130
140
  function noteSkippedSseServer(name) {
131
141
  if (skippedSseServers.has(name))
132
142
  return;
@@ -592,6 +602,11 @@ export const CodexDriver = {
592
602
  if (turn.integrations?.browser) {
593
603
  mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
594
604
  }
605
+ // A custom server named like one in the user's own config.toml would
606
+ // be merged with it by the `-c` override — a stdio command over a
607
+ // remote url is "invalid configuration" and kills the turn before the
608
+ // model is asked. Such a server mounts under a name of its own.
609
+ const declaredInCodexConfig = codexConfigMcpServerNames(env);
595
610
  for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
596
611
  // codex speaks streamable HTTP to a remote server, not the older
597
612
  // SSE transport: such an entry still reaches Claude bots, and is
@@ -600,7 +615,10 @@ export const CodexDriver = {
600
615
  noteSkippedSseServer(name);
601
616
  continue;
602
617
  }
603
- mountMcpServer(appServerArgs, env, name, server, false);
618
+ const mountName = mountedMcpServerName(name, declaredInCodexConfig);
619
+ if (mountName !== name)
620
+ noteRenamedMcpServer(name, mountName);
621
+ mountMcpServer(appServerArgs, env, mountName, server, false);
604
622
  }
605
623
  if (turn.integrations?.phone) {
606
624
  const bridge = turn.integrations.phone;
@@ -1039,6 +1057,11 @@ export const CodexDriver = {
1039
1057
  ok: item.status !== "failed" && item.status !== "declined",
1040
1058
  output: toolDetailPreview(item.type === "commandExecution" ? { output: item.aggregatedOutput, exitCode: item.exitCode } : item.type === "mcpToolCall" ? item.error ?? item.result : item.type === "fileChange" ? item.changes : item.action),
1041
1059
  });
1060
+ if (item.type === "mcpToolCall") {
1061
+ for (const img of extractMcpImages(item.result)) {
1062
+ emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_image", data: img.data });
1063
+ }
1064
+ }
1042
1065
  }
1043
1066
  else if (item.type === "reasoning") {
1044
1067
  emit({ ...base(threadId, turnId), type: "item.updated", itemType: "reasoning", tokens: null });
@@ -1282,10 +1305,15 @@ export const CodexDriver = {
1282
1305
  });
1283
1306
  effectiveConfig = configured?.config;
1284
1307
  }
1285
- catch {
1308
+ catch (error) {
1286
1309
  // Do not expose a possibly secret-bearing native config error or
1287
- // overwrite unknown instructions with an empty fallback.
1288
- throw new Error("Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
1310
+ // overwrite unknown instructions with an empty fallback. A config
1311
+ // codex itself rejected is the one case worth naming: the person
1312
+ // can act on it, and the message carries no credential.
1313
+ const invalid = error instanceof CodexRpcError && /^invalid configuration\b/i.test(error.message);
1314
+ throw new Error(invalid
1315
+ ? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
1316
+ : "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
1289
1317
  }
1290
1318
  const developerInstructions = codexDeveloperInstructions(effectiveConfig, turn.system ?? "");
1291
1319
  let approvalParams;
@@ -2,8 +2,9 @@ import { createOpenAIChatRuntime } from "./openai-chat.js";
2
2
  const DRIVER_KIND = "grok";
3
3
  const DEFAULT_URL = "https://api.x.ai/v1";
4
4
  const MODELS = {
5
- default: "grok-4",
5
+ default: "grok-4.7",
6
6
  options: [
7
+ { id: "grok-4.7", label: "Grok 4.7", contextWindow: 500_000 },
7
8
  { id: "grok-4", label: "Grok 4" },
8
9
  { id: "grok-4-fast", label: "Grok 4 Fast" },
9
10
  { id: "grok-3-mini", label: "Grok 3 Mini" },
@@ -115,11 +115,67 @@ export function createOpenAIChatRuntime(options) {
115
115
  const reader = response.body.getReader();
116
116
  const decoder = new TextDecoder();
117
117
  let buffer = "";
118
+ const consumeDataLine = (line, atEof = false) => {
119
+ if (!line.startsWith("data:"))
120
+ return false;
121
+ const data = line.slice(5).trim();
122
+ if (data === "[DONE]")
123
+ return true;
124
+ let chunk;
125
+ try {
126
+ chunk = JSON.parse(data);
127
+ }
128
+ catch {
129
+ // A tail left in the buffer when the socket closed is an INCOMPLETE
130
+ // frame, not a bad one: a stop, an abort or a dropped connection all
131
+ // end mid-frame. Only a properly newline-terminated frame that will
132
+ // not parse means the provider actually sent something malformed.
133
+ // Counting the tail here turned a stopped turn into a hard,
134
+ // non-retryable failure (`calls.finish(finishReason, malformedFrame)`).
135
+ if (!atEof)
136
+ malformedFrame = true;
137
+ return false;
138
+ }
139
+ const chunkError = providerError(chunk);
140
+ if (chunkError)
141
+ throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
142
+ const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
143
+ const delta = choice?.delta;
144
+ if (object(delta))
145
+ sawChoice = true;
146
+ if (delta?.function_call)
147
+ throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
148
+ calls.add(delta?.tool_calls, true);
149
+ details.add(delta?.reasoning_details);
150
+ if (choice?.finish_reason)
151
+ finishReason = choice.finish_reason;
152
+ const reasoningPart = delta?.reasoning_content ?? delta?.reasoning;
153
+ if (typeof reasoningPart === "string")
154
+ protocolReasoning += reasoningPart;
155
+ const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
156
+ ? reasoningPart
157
+ : "";
158
+ const contentDelta = typeof delta?.content === "string" ? delta.content : "";
159
+ if (reasoningDelta) {
160
+ reasoning += reasoningDelta;
161
+ onDelta?.(reasoningDelta, "reasoning_text");
162
+ }
163
+ if (contentDelta) {
164
+ text += contentDelta;
165
+ onDelta?.(contentDelta, "assistant_text");
166
+ }
167
+ if (chunk.usage)
168
+ usage = usageFrom(chunk.usage);
169
+ return false;
170
+ };
118
171
  try {
119
172
  readLoop: for (;;) {
120
173
  const { done, value } = await reader.read();
121
174
  if (done) {
122
175
  buffer += decoder.decode();
176
+ const line = buffer.trim();
177
+ if (line && line !== "data: [DONE]")
178
+ consumeDataLine(line, true);
123
179
  // MiniMax's api.minimax.io/v1 closes the connection after the
124
180
  // finish_reason chunk and never sends `[DONE]`.
125
181
  if (buffer.trim() === "data: [DONE]" || finishReason)
@@ -146,49 +202,8 @@ export function createOpenAIChatRuntime(options) {
146
202
  while ((newline = buffer.indexOf("\n")) !== -1) {
147
203
  const line = buffer.slice(0, newline).trim();
148
204
  buffer = buffer.slice(newline + 1);
149
- if (!line.startsWith("data:"))
150
- continue;
151
- const data = line.slice(5).trim();
152
- if (data === "[DONE]")
205
+ if (consumeDataLine(line))
153
206
  break readLoop;
154
- let chunk;
155
- try {
156
- chunk = JSON.parse(data);
157
- }
158
- catch {
159
- malformedFrame = true;
160
- continue;
161
- }
162
- const chunkError = providerError(chunk);
163
- if (chunkError)
164
- throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
165
- const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
166
- const delta = choice?.delta;
167
- if (object(delta))
168
- sawChoice = true;
169
- if (delta?.function_call)
170
- throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
171
- calls.add(delta?.tool_calls, true);
172
- details.add(delta?.reasoning_details);
173
- if (choice?.finish_reason)
174
- finishReason = choice.finish_reason;
175
- const reasoningPart = delta?.reasoning_content ?? delta?.reasoning;
176
- if (typeof reasoningPart === "string")
177
- protocolReasoning += reasoningPart;
178
- const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
179
- ? reasoningPart
180
- : "";
181
- const contentDelta = typeof delta?.content === "string" ? delta.content : "";
182
- if (reasoningDelta) {
183
- reasoning += reasoningDelta;
184
- onDelta?.(reasoningDelta, "reasoning_text");
185
- }
186
- if (contentDelta) {
187
- text += contentDelta;
188
- onDelta?.(contentDelta, "assistant_text");
189
- }
190
- if (chunk.usage)
191
- usage = usageFrom(chunk.usage);
192
207
  }
193
208
  }
194
209
  }
@@ -383,7 +398,13 @@ export function createOpenAIChatRuntime(options) {
383
398
  throw new ChatProtocolError("tool arguments must be a JSON object");
384
399
  tools.validate(call.function.name, args);
385
400
  const inputPreview = preview(args);
386
- const allowed = await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
401
+ // Full access is the person's explicit grant to answer every
402
+ // prompt. This runtime has no provider reviewer to hand it to,
403
+ // so it is honoured here: without it every single tool call on
404
+ // an OpenAI-compatible engine stops for a card, and a Chief's
405
+ // delegated Full access cannot help either.
406
+ const allowed = turn.approvalMode === "full"
407
+ || await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
387
408
  abort.signal.throwIfAborted();
388
409
  emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
389
410
  title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
@@ -489,9 +510,9 @@ export function createOpenAIChatRuntime(options) {
489
510
  return () => listeners.delete(listener);
490
511
  },
491
512
  },
492
- generateText: async (prompt) => {
513
+ generateText: async (prompt, { signal } = {}) => {
493
514
  const model = options.generateModel?.() ?? options.models().default;
494
- const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false);
515
+ const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal);
495
516
  if (toolCalls.length)
496
517
  throw new ChatProtocolError("provider returned tool calls to a text-only helper");
497
518
  return text.trim() ? text : reasoning;