openmausbot 0.1.86 → 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 (166) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
  7. package/dist/assets/channel-taCNRlUU.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
  34. package/dist/assets/index-BwdMAQ6p.css +1 -0
  35. package/dist/assets/index-ByJennr7.js +581 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/container-mcp.js +1029 -164
  63. package/dist-server/drivers/agents-proxy.js +64 -10
  64. package/dist-server/drivers/phone-proxy.js +33 -2
  65. package/dist-server/index.js +14577 -9001
  66. package/dist-server/local-computer-proxy.js +3 -3
  67. package/dist-server/local-computer.js +53 -17
  68. package/dist-server/openmausbot.js +3981 -1165
  69. package/dist-server/pair-cli.js +3981 -1165
  70. package/dist-server/permission-proxy.js +29 -9
  71. package/dist-server/server/attachments.js +55 -0
  72. package/dist-server/server/auto-approve.js +4 -3
  73. package/dist-server/server/auto-vm-claims.js +1 -0
  74. package/dist-server/server/bot-package.js +18 -292
  75. package/dist-server/server/bot-profile.js +5 -17
  76. package/dist-server/server/box-create-idempotency.js +3 -3
  77. package/dist-server/server/box.js +57 -55
  78. package/dist-server/server/browser-proxy.js +3 -2
  79. package/dist-server/server/browser-runtime.js +37 -1
  80. package/dist-server/server/cli.js +2 -2
  81. package/dist-server/server/command-allowlist.js +154 -0
  82. package/dist-server/server/computer-wait.js +22 -9
  83. package/dist-server/server/config.js +59 -9
  84. package/dist-server/server/connector-verdict.js +141 -0
  85. package/dist-server/server/container-computer.js +1 -1
  86. package/dist-server/server/default-model-selection.js +9 -0
  87. package/dist-server/server/delegations.js +85 -34
  88. package/dist-server/server/drivers/acp/core.js +57 -15
  89. package/dist-server/server/drivers/agents-call.js +42 -3
  90. package/dist-server/server/drivers/agents-catalog.js +34 -4
  91. package/dist-server/server/drivers/boxagent.js +174 -36
  92. package/dist-server/server/drivers/builtIn.js +2 -0
  93. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  94. package/dist-server/server/drivers/chat-images.js +54 -0
  95. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  96. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  97. package/dist-server/server/drivers/claude.js +7 -2
  98. package/dist-server/server/drivers/codex-instructions.js +48 -6
  99. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  100. package/dist-server/server/drivers/codex.js +95 -30
  101. package/dist-server/server/drivers/mistral.js +115 -0
  102. package/dist-server/server/drivers/openai-chat.js +155 -32
  103. package/dist-server/server/drivers/openai-compat.js +1 -0
  104. package/dist-server/server/drivers/permission-command.js +35 -0
  105. package/dist-server/server/drivers/phone-proxy.js +47 -2
  106. package/dist-server/server/drivers/pi.js +75 -4
  107. package/dist-server/server/drivers/prompt-split.js +87 -0
  108. package/dist-server/server/fleet.js +1 -1
  109. package/dist-server/server/group-thread-usage.js +103 -0
  110. package/dist-server/server/harness/registry.js +1 -0
  111. package/dist-server/server/hosted-models.js +1 -1
  112. package/dist-server/server/index.js +1385 -322
  113. package/dist-server/server/local-computer.js +72 -23
  114. package/dist-server/server/managed-desktop.js +1 -1
  115. package/dist-server/server/mcp-bridge.js +6 -6
  116. package/dist-server/server/new-bot-defaults.js +115 -0
  117. package/dist-server/server/org-library.js +1051 -0
  118. package/dist-server/server/package-export.js +627 -39
  119. package/dist-server/server/package-import.js +458 -0
  120. package/dist-server/server/package-parts.js +87 -0
  121. package/dist-server/server/peer-approval.js +16 -7
  122. package/dist-server/server/peer-roster.js +10 -0
  123. package/dist-server/server/presets.js +492 -0
  124. package/dist-server/server/provider-key-check.js +3 -2
  125. package/dist-server/server/published-teams.js +80 -0
  126. package/dist-server/server/room-handoffs.js +19 -3
  127. package/dist-server/server/routes/bot-presets.js +25 -0
  128. package/dist-server/server/routine-requests.js +6 -3
  129. package/dist-server/server/routines.js +41 -1
  130. package/dist-server/server/section-context.js +10 -0
  131. package/dist-server/server/skills.js +34 -61
  132. package/dist-server/server/steer-queue.js +3 -2
  133. package/dist-server/server/store.js +259 -15
  134. package/dist-server/server/surface.js +10 -4
  135. package/dist-server/server/system-prompt.js +12 -4
  136. package/dist-server/server/team-backup.js +6 -2
  137. package/dist-server/server/team-computers.js +10 -0
  138. package/dist-server/server/team-library.js +6 -2
  139. package/dist-server/server/team-manifest.js +4 -3
  140. package/dist-server/server/team-setup-requests.js +16 -5
  141. package/dist-server/server/thread-events.js +17 -1
  142. package/dist-server/server/turn-context.js +2 -0
  143. package/dist-server/server/turn-watchdog.js +12 -3
  144. package/dist-server/server/usage-ledger.js +12 -1
  145. package/dist-server/server/vps-computer.js +139 -51
  146. package/dist-server/server/vps-container-mcp.js +7 -0
  147. package/dist-server/server/vps-ssh.js +36 -7
  148. package/dist-server/server/workspace-backup-policy.js +12 -0
  149. package/dist-server/server/workspace-backup.js +2 -2
  150. package/dist-server/server/workspace.js +1 -1
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/ask-question.js +188 -12
  153. package/dist-server/shared/bot-profile.js +15 -0
  154. package/dist-server/shared/command-allowlist.js +1 -0
  155. package/dist-server/shared/credential-request.js +1 -1
  156. package/dist-server/shared/package-format.js +967 -0
  157. package/dist-server/shared/skill-md.js +64 -0
  158. package/dist-server/shared/team-backup.js +7 -0
  159. package/dist-server/shared/wire.js +4 -0
  160. package/dist-server/vps-container-mcp.js +1129 -170
  161. package/enterprise/README.md +1 -1
  162. package/package.json +1 -1
  163. package/dist/assets/channel-BXFkgus9.js +0 -1
  164. package/dist/assets/index-BGgqlWPJ.css +0 -1
  165. package/dist/assets/index-BtHo8ZKl.js +0 -577
  166. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -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";
@@ -1199,6 +1200,7 @@ export const ClaudeDriver = {
1199
1200
  // parallel bot work must use the harness's durable delegate_bot path.
1200
1201
  env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
1201
1202
  const cwd = turn.cwd ?? homedir();
1203
+ const commandCwd = permissionLaunchCwd(cwd);
1202
1204
  // Everything that shapes the process, minus session/turn-specific temp
1203
1205
  // paths. Their contents are represented directly in the key instead.
1204
1206
  const privateFileFlags = new Set(["--mcp-config", "--settings"]);
@@ -1239,7 +1241,7 @@ export const ClaudeDriver = {
1239
1241
  }, turnId, broker: live.broker });
1240
1242
  emit({ ...base(threadId, turnId), type: "turn.started" });
1241
1243
  const volatile = turn.systemVolatile ?? "";
1242
- const message = volatile === live.volatile
1244
+ const message = volatile === live.volatile && !turn.mentionTurn
1243
1245
  ? promptMsg
1244
1246
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1245
1247
  live.volatile = volatile;
@@ -1323,6 +1325,9 @@ export const ClaudeDriver = {
1323
1325
  requestType: ask.kind,
1324
1326
  tool: ask.tool,
1325
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,
1326
1331
  nativeReview,
1327
1332
  // the proxy hands Claude its own suggested rules on `always`;
1328
1333
  // host control stays one action at a time
@@ -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,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
+ }
@@ -1,3 +1,4 @@
1
+ import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
1
2
  // Codex driver — upstream CodexDriver skeleton over agentcal's
2
3
  // drivers/codex.js runtime: the official `codex` CLI headless over its
3
4
  // app-server JSON-RPC protocol (newline-delimited JSON on stdio).
@@ -21,12 +22,15 @@ import { codexLocalProviderArgs } from "./local-inject.js";
21
22
  import { augmentedPath, splitCliString } from "../env-path.js";
22
23
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
23
24
  import { appendNative } from "./native.js";
25
+ import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
24
26
  import { commandSummary, toolDetailPreview } from "../tool-summary.js";
25
27
  import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
28
+ import { volatileContextNote, withContextNote } from "./prompt-split.js";
26
29
  import { CodexDeviceAuthController } from "./codex-device-auth.js";
27
30
  import { codexAccountEmail } from "./codex-identity.js";
28
31
  import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
29
32
  import { extractMcpImages } from "../mcp-tool-images.js";
33
+ import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
30
34
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
31
35
  const DRIVER_KIND = "codex";
32
36
  const ASTRA_MODEL_ID = "gpt-6-astra";
@@ -126,7 +130,6 @@ export function managedCodexArgs(config) {
126
130
  "-c", "shell_environment_policy.ignore_default_excludes=false",
127
131
  ];
128
132
  }
129
- const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
130
133
  const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
131
134
  const skippedSseServers = new Set();
132
135
  const renamedMcpServers = new Set();
@@ -275,6 +278,21 @@ function namedApprovalParams(mode) {
275
278
  },
276
279
  };
277
280
  }
281
+ /** Keep the turn on the complete sandbox resolved by native start/resume. */
282
+ function withResolvedSandbox(params, session) {
283
+ const requested = plainRecord(params.turn.sandboxPolicy);
284
+ // Named permission profiles own their policy; do not mix both selectors.
285
+ if (!requested)
286
+ return params;
287
+ const sandbox = plainRecord(plainRecord(session)?.sandbox);
288
+ if (!sandbox || typeof sandbox.type !== "string") {
289
+ throw new Error("Codex did not return its resolved sandbox policy. Update Codex, then retry; the turn was not started because its permissions could not be verified.");
290
+ }
291
+ if (sandbox.type !== requested.type) {
292
+ throw new Error("Codex did not apply the requested sandbox mode; cannot safely start the turn.");
293
+ }
294
+ return { ...params, turn: { ...params.turn, sandboxPolicy: sandbox } };
295
+ }
278
296
  function effectiveApprovalPolicy(value) {
279
297
  if (value === "untrusted" || value === "on-request" || value === "never")
280
298
  return value;
@@ -587,7 +605,7 @@ export const CodexDriver = {
587
605
  const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
588
606
  const launchAttempt = async (attempt) => {
589
607
  const env = childEnv();
590
- const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
608
+ const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs()];
591
609
  if (turn.integrations?.composio) {
592
610
  mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
593
611
  }
@@ -626,6 +644,7 @@ export const CodexDriver = {
626
644
  const prefix = "mcp_servers.openmausbot_phone";
627
645
  appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(bridge.command)}`, "-c", `${prefix}.args=${JSON.stringify(bridge.args)}`, "-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(bridge.env))}`, "-c", `${prefix}.default_tools_approval_mode="auto"`);
628
646
  }
647
+ const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
629
648
  const child = spawnCli(config.cli, appServerArgs, {
630
649
  cwd: turn.cwd ?? homedir(),
631
650
  env,
@@ -838,23 +857,22 @@ export const CodexDriver = {
838
857
  }
839
858
  return;
840
859
  }
841
- // One ask card carries one question honestly: its choices would come
842
- // from the first question alone and its one reply (including the
843
- // timeout note) would be copied into every question id (#1237).
844
- // Refuse the bundled call with a teaching error instead of
845
- // fabricating per-question answers.
846
- if (isQuestion && (!Array.isArray(params.questions) || params.questions.length !== 1)) {
847
- const bundled = Array.isArray(params.questions) && params.questions.length > 1;
860
+ // The whole set rides one card, answered per id: the protocol pairs
861
+ // each question with its own id, and the card's single reply is
862
+ // mapped back block by block (or as the flat single-question
863
+ // fallback). The only refusal left is an ask with nothing answerable
864
+ // — #1237 is closed: a bundle no longer copies one answer into
865
+ // every id.
866
+ const protocolQuestions = isQuestion ? parseProtocolAskQuestions(params.questions) : null;
867
+ if (isQuestion && !protocolQuestions) {
848
868
  send({
849
869
  jsonrpc: "2.0",
850
870
  id: msg.id,
851
871
  error: {
852
872
  code: -32602,
853
- message: bundled
854
- ? `ask supports one question per call; this request bundled ${params.questions.length}. Split it into separate asks, one question each.`
855
- : Array.isArray(params.questions)
856
- ? "ask supports one question per call; this request sent none."
857
- : "ask supports one question per call; params.questions must be an array with exactly one question.",
873
+ message: Array.isArray(params.questions)
874
+ ? "ask needs at least one answerable question — a string id and question text each; this request sent none."
875
+ : "ask needs params.questions to be an array of questions, each with a string id and question text.",
858
876
  },
859
877
  });
860
878
  return;
@@ -894,23 +912,36 @@ export const CodexDriver = {
894
912
  ? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
895
913
  : typeof params.command === "string"
896
914
  ? params.command
897
- : Array.isArray(params.questions)
898
- ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
899
- : typeof params.reason === "string"
900
- ? params.reason
901
- : tool;
902
- const choices = isQuestion
903
- ? (params.questions?.[0]?.options ?? []).map((o) => o.label).slice(0, 5)
904
- : undefined;
915
+ : protocolQuestions
916
+ ? protocolQuestions.map(({ question }) => question.question).join(" · ")
917
+ : Array.isArray(params.questions)
918
+ ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
919
+ : typeof params.reason === "string"
920
+ ? params.reason
921
+ : tool;
922
+ // Flat choices only when one non-multiselect question can actually
923
+ // be answered by a bare reply; a bundle's first-question buttons
924
+ // would be an unusable lie for flat clients.
925
+ const choices = isQuestion && protocolQuestions ? questionChoices(protocolQuestions.map(({ question }) => question)) : undefined;
905
926
  const finish = (behavior, message, source = "user") => {
906
927
  if (!asks.delete(requestId))
907
928
  return;
908
929
  clearTimeout(timer);
909
930
  if (isQuestion) {
910
- const answers = {};
911
- for (const q of Array.isArray(params.questions) ? params.questions : []) {
912
- answers[q.id] = { answers: [message || QUESTION_TIMEOUT_NOTE] };
913
- }
931
+ // Only the person's reply is filed: Q:/A: blocks mapped to the
932
+ // id that asked them, or the flat fallback for a single
933
+ // question. Timeout and turn teardown send empty answers so
934
+ // every id reads unanswered — system notes never occupy the
935
+ // slot the model reads as the person's words (the rule
936
+ // permission-proxy already follows).
937
+ const mapped = behavior === "answer" && source === "user" && typeof message === "string"
938
+ ? questionAnswersById(message, protocolQuestions ?? [])
939
+ : {};
940
+ // Null prototype so an opaque id like __proto__ becomes a real
941
+ // answer key instead of hitting the inherited setter.
942
+ const answers = Object.create(null);
943
+ for (const [id, answer] of Object.entries(mapped))
944
+ answers[id] = { answers: [answer] };
914
945
  send({ jsonrpc: "2.0", id: msg.id, result: { answers } });
915
946
  }
916
947
  else {
@@ -922,7 +953,11 @@ export const CodexDriver = {
922
953
  }
923
954
  emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source });
924
955
  };
925
- const timer = setTimeout(() => (isQuestion ? finish("answer", QUESTION_TIMEOUT_NOTE, "timeout") : finish("deny", DENY_TIMEOUT_NOTE, "timeout")), 15 * 60_000);
956
+ const timer = setTimeout(
957
+ // A timed-out question resolves as denied, not answered: the card
958
+ // closes with no reply (answers stay empty either way), and the
959
+ // resolve event must not claim an answer that never happened.
960
+ () => finish("deny", isQuestion ? undefined : DENY_TIMEOUT_NOTE, "timeout"), 15 * 60_000);
926
961
  timer.unref?.();
927
962
  asks.set(requestId, finish);
928
963
  emit({
@@ -932,9 +967,15 @@ export const CodexDriver = {
932
967
  requestType: isQuestion ? "question" : "permission",
933
968
  tool,
934
969
  summary,
970
+ command: method === "execCommandApproval" || method === "item/commandExecution/requestApproval"
971
+ ? permissionCommand(params.command, params.cwd ?? (
972
+ // Helpers may have a different workspace from their parent.
973
+ !params.threadId || params.threadId === codexThreadId ? commandCwd : undefined)) : undefined,
935
974
  choices,
975
+ ...(protocolQuestions ? { questions: protocolQuestions.map((pair) => pair.question) } : {}),
936
976
  approvalScope: controlsHost ? "local-computer" : undefined,
937
- requiresExplicitApproval: isAdditionalPermission || undefined,
977
+ requiresExplicitApproval: isAdditionalPermission || ((method === "execCommandApproval" || method === "item/commandExecution/requestApproval") &&
978
+ (params.additionalPermissions != null || params.networkApprovalContext != null)) || undefined,
938
979
  });
939
980
  };
940
981
  const handleNotification = (msg) => {
@@ -1315,7 +1356,18 @@ export const CodexDriver = {
1315
1356
  ? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
1316
1357
  : "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
1317
1358
  }
1318
- const developerInstructions = codexDeveloperInstructions(effectiveConfig, turn.system ?? "");
1359
+ // Only the stable half of the prompt belongs in the developer slot:
1360
+ // it is the part that must survive compaction unchanged, and any
1361
+ // change to it invalidates the provider's cached prefix. The volatile
1362
+ // half (memory, mentions, outstanding teammate work) is delivered
1363
+ // inside the turn that changed it, after the cached prefix, the same
1364
+ // contract SendTurnInput.systemStable documents. Without the split
1365
+ // the driver keeps the previous single-block behaviour.
1366
+ const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
1367
+ ? turn.systemStable
1368
+ : null;
1369
+ const promptSplit = stableInstructions !== null;
1370
+ const developerInstructions = codexDeveloperInstructions(effectiveConfig, stableInstructions ?? turn.system ?? "");
1319
1371
  let approvalParams;
1320
1372
  if (approvalMode === "custom") {
1321
1373
  // config/read returns the effective global + project config for this
@@ -1358,6 +1410,7 @@ export const CodexDriver = {
1358
1410
  approvalParams = approvalParams.fallback;
1359
1411
  resumed = await resumeThread();
1360
1412
  }
1413
+ approvalParams = withResolvedSandbox(approvalParams, resumed);
1361
1414
  codexThreadId = resumed?.thread?.id ?? cursor;
1362
1415
  resumedNativeThread = true;
1363
1416
  }
@@ -1403,12 +1456,20 @@ export const CodexDriver = {
1403
1456
  approvalParams = approvalParams.fallback;
1404
1457
  started = await startThread();
1405
1458
  }
1459
+ approvalParams = withResolvedSandbox(approvalParams, started);
1406
1460
  codexThreadId = started?.thread?.id ?? null;
1407
1461
  startedModel = started?.model ?? null;
1408
1462
  }
1409
1463
  if (!codexThreadId)
1410
1464
  throw new Error("Codex did not return a native thread id");
1411
- await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
1465
+ const { deliverVolatile, hadVolatile, commitVolatile } = await syncCodexInstructions(threadId, codexThreadId, developerInstructions, promptSplit ? turn.systemVolatile ?? "" : "", resumedNativeThread, request, Boolean(turn.mentionTurn));
1466
+ // A changed volatile half rides the next user input as a labelled
1467
+ // context block. It never touches the developer slot, so an
1468
+ // ordinary memory write or roster change neither appends a second
1469
+ // copy of the prompt to history nor re-uploads the conversation.
1470
+ if (deliverVolatile) {
1471
+ promptText = withContextNote(volatileContextNote(promptSplit ? turn.systemVolatile ?? "" : "", hadVolatile), promptText);
1472
+ }
1412
1473
  emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
1413
1474
  const turnInput = [
1414
1475
  ...(promptText ? [{ type: "text", text: promptText }] : []),
@@ -1442,6 +1503,10 @@ export const CodexDriver = {
1442
1503
  approvalParams = approvalParams.fallback;
1443
1504
  await startTurn();
1444
1505
  }
1506
+ // turn/start accepted the input: only now may the receipt claim the
1507
+ // volatile half was delivered, so a rejected turn redelivers on retry.
1508
+ if (commitVolatile)
1509
+ commitVolatile();
1445
1510
  }
1446
1511
  catch (e) {
1447
1512
  const failure = e instanceof Error ? e : { text: String(e) };
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import { createOpenAIChatRuntime } from "./openai-chat.js";
3
+ const DEFAULT_URL = "https://api.mistral.ai/v1";
4
+ const DEFAULT_MODELS = {
5
+ default: "mistral-large-latest",
6
+ options: [
7
+ { id: "mistral-large-latest", label: "Mistral Large (latest)" },
8
+ { id: "mistral-small-latest", label: "Mistral Small (latest)" },
9
+ ],
10
+ };
11
+ const configSchema = z.object({
12
+ url: z.string().trim().url().default(DEFAULT_URL),
13
+ model: z.string().trim().min(1).optional(),
14
+ tools: z.boolean().optional(),
15
+ });
16
+ function decodeConfig(raw) {
17
+ const config = configSchema.parse(raw ?? {});
18
+ config.url = config.url.replace(/\/+$/, "");
19
+ const url = new URL(config.url);
20
+ if (url.protocol !== "https:" && !(url.protocol === "http:" &&
21
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
22
+ throw new Error("Mistral requires HTTPS, except for a local test endpoint.");
23
+ }
24
+ return config;
25
+ }
26
+ const modelCard = z.object({
27
+ id: z.string().min(1),
28
+ name: z.string().nullable().optional(),
29
+ aliases: z.array(z.string()).optional(),
30
+ archived: z.boolean().optional(),
31
+ capabilities: z.object({ completion_chat: z.boolean().optional() }).optional(),
32
+ max_context_length: z.number().int().positive().optional(),
33
+ });
34
+ /** Mistral can return plain text or a list of typed content chunks. */
35
+ function contentText(content) {
36
+ if (typeof content === "string")
37
+ return content;
38
+ if (!Array.isArray(content))
39
+ return "";
40
+ return content.map((chunk) => chunk?.type === "text" && typeof chunk.text === "string" ? chunk.text : "").join("");
41
+ }
42
+ export const MistralDriver = {
43
+ driverKind: "mistral",
44
+ metadata: { displayName: "Mistral (API)", supportsMultipleInstances: true, access: "api" },
45
+ models: DEFAULT_MODELS,
46
+ install: {
47
+ docsUrl: "https://console.mistral.ai/api-keys",
48
+ signInCommand: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY on the server.",
49
+ },
50
+ decodeConfig,
51
+ defaultConfig: () => decodeConfig({}),
52
+ async create(input) {
53
+ const { config } = input;
54
+ const apiKey = (input.environment.MISTRAL_API_KEY ?? process.env.MISTRAL_API_KEY ?? "").trim();
55
+ const withConfiguredModel = (options) => {
56
+ const preferred = config.model ?? DEFAULT_MODELS.default;
57
+ if (config.model && !options.some((option) => option.id === config.model)) {
58
+ options = [{ id: config.model, label: config.model }, ...options];
59
+ }
60
+ return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
61
+ };
62
+ let catalog = withConfiguredModel(DEFAULT_MODELS.options);
63
+ const refreshModels = async () => {
64
+ if (!apiKey)
65
+ return;
66
+ try {
67
+ const response = await fetch(`${config.url}/models`, {
68
+ headers: { authorization: `Bearer ${apiKey}` },
69
+ redirect: "error",
70
+ signal: AbortSignal.timeout(8_000),
71
+ });
72
+ if (!response.ok)
73
+ return;
74
+ const json = await response.json();
75
+ const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
76
+ const options = [];
77
+ const seen = new Set();
78
+ for (const row of rows) {
79
+ const parsed = modelCard.safeParse(row);
80
+ if (!parsed.success || parsed.data.archived || parsed.data.capabilities?.completion_chat === false)
81
+ continue;
82
+ const card = parsed.data;
83
+ for (const id of [card.id, ...(card.aliases ?? [])]) {
84
+ if (!id.trim() || seen.has(id))
85
+ continue;
86
+ seen.add(id);
87
+ options.push({ id, label: id === card.id ? card.name?.trim() || id : id,
88
+ ...(card.max_context_length ? { contextWindow: card.max_context_length } : {}) });
89
+ }
90
+ }
91
+ if (options.length)
92
+ catalog = withConfiguredModel(options);
93
+ }
94
+ catch {
95
+ // A failed refresh keeps the last usable catalog, including custom models.
96
+ }
97
+ };
98
+ if (apiKey)
99
+ void refreshModels();
100
+ return createOpenAIChatRuntime({
101
+ input, driverKind: "mistral", apiKey, apiUrl: config.url,
102
+ tools: config.tools, models: () => catalog, refreshModels, contentText,
103
+ requestBody: (model, messages, stream) => ({ model, messages, stream }),
104
+ httpErrorLabel: "Mistral",
105
+ missingKeyError: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY.",
106
+ unavailableReason: "No Mistral API key — open Settings → Connections.",
107
+ timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
108
+ nativeLog: {
109
+ source: "mistral.chat.completions",
110
+ outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
111
+ incoming: ({ text, usage }) => ({ textLength: text.length, usage }),
112
+ },
113
+ });
114
+ },
115
+ };