openmausbot 0.1.86 → 0.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -5,6 +5,9 @@ import { createHash, randomUUID } from "node:crypto";
5
5
  import { z } from "zod";
6
6
  import { SPAWNED_PROXIES } from "./proxy-paths.js";
7
7
  import { managedConnectorUnavailableReason } from "../shared/connector-availability.js";
8
+ import { CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
9
+ import { CONNECTOR_ALLOWED_TOOLS_ENV, CONNECTOR_ALLOWED_TOOLS_MAX_BYTES, CONNECTOR_SERVICE_SLUGS_ENV, CONNECTOR_SERVICE_SLUGS_MAX_BYTES, serializeConnectorAllowedTools, serializeConnectorServiceSlugs, } from "./connector-advertisement.js";
10
+ import { serviceSlugFor, serviceSlugForCandidates } from "./connector-verdict.js";
8
11
  const DEFAULT_BACKEND_ORIGIN = "https://backend.composio.dev";
9
12
  function apiBase() {
10
13
  return (process.env.OMB_COMPOSIO_API ?? `${DEFAULT_BACKEND_ORIGIN}/api/v3.1`).replace(/\/$/, "");
@@ -443,6 +446,30 @@ const NEEDS_AUTH_CONFIG = /does not manage auth|auth[_ ]?config/i;
443
446
  export async function mcpIntegration(cfg, context) {
444
447
  if (!configured(cfg))
445
448
  return null;
449
+ // Connector grants 3/5: a grants record rides to the bridge as a capped
450
+ // JSON env var so tools/list is filtered down to what the bot may call.
451
+ // No record (legacy all-tools bots) or one past the cap mounts without
452
+ // the var — the bridge then relays the unfiltered list and every call is
453
+ // still judged by the slice-2 verdict on the relay endpoint.
454
+ const allowlist = context.connectorTools === undefined
455
+ ? undefined
456
+ : serializeConnectorAllowedTools(context.connectorTools);
457
+ if (allowlist?.oversized) {
458
+ console.warn(`[composio] connector allowlist for bot ${context.botId} exceeds ${CONNECTOR_ALLOWED_TOOLS_MAX_BYTES} bytes;`
459
+ + " tools/list stays unfiltered and grants are enforced per call");
460
+ }
461
+ // The connected-service slugs ride alongside the allowlist so the
462
+ // bridge resolves underscored services (bland_ai) instead of letting a
463
+ // plain-prefix grant (bland) widen them. An unreachable catalog omits
464
+ // the var and the filter keeps its plain-split fallback; legacy
465
+ // all-tools bots mount without it because their list is unfiltered.
466
+ const serviceSlugs = context.connectorTools === undefined
467
+ ? undefined
468
+ : serializeConnectorServiceSlugs(await connectedServiceSlugs(cfg));
469
+ if (serviceSlugs?.oversized) {
470
+ console.warn(`[composio] connector service slugs for bot ${context.botId} exceed ${CONNECTOR_SERVICE_SLUGS_MAX_BYTES} bytes;`
471
+ + " tools/list falls back to plain-prefix resolution");
472
+ }
446
473
  return {
447
474
  command: process.execPath,
448
475
  args: [SPAWNED_PROXIES.connectors],
@@ -460,6 +487,8 @@ export async function mcpIntegration(cfg, context) {
460
487
  OMB_CONNECTOR_TOKEN: context.commsToken,
461
488
  OMB_BOT_ID: context.botId,
462
489
  OMB_THREAD_ID: context.threadId,
490
+ ...(allowlist?.env ? { [CONNECTOR_ALLOWED_TOOLS_ENV]: allowlist.env } : {}),
491
+ ...(serviceSlugs?.env ? { [CONNECTOR_SERVICE_SLUGS_ENV]: serviceSlugs.env } : {}),
463
492
  },
464
493
  };
465
494
  }
@@ -672,6 +701,266 @@ export async function connectedServices(cfg) {
672
701
  ]);
673
702
  return allServiceStates(summarizeAccounts(accounts, []), toolkits);
674
703
  }
704
+ /** Semantic validation for a connectorTools patch (issue #1737): slugs
705
+ * must name a connected service, and every listed tool name must carry its
706
+ * own service prefix (GMAIL_SEND_EMAIL under "gmail") — a mismatched name
707
+ * would be granted yet permanently refused at call time. The prefix check
708
+ * resolves against the connected-service slugs, so an underscored service
709
+ * (bland_ai) keeps its BLAND_AI_* names and a plain-prefix grant cannot
710
+ * capture them; it always runs, degrading to the plain split when the
711
+ * connection inventory is unreachable. Only the connection-dependent and
712
+ * catalog checks fail open — an unreachable connection inventory, the
713
+ * curated fallback, or a catalog walk that did not reach its end never
714
+ * block the patch, because call-time enforcement (slice 2) stays the
715
+ * authority either way. Returns the rejection message or null. */
716
+ export async function validateConnectorGrants(cfg, grants) {
717
+ const connected = await connectedServices(cfg).catch(() => null);
718
+ const candidates = connected ? Object.keys(connected) : [];
719
+ for (const [slug, grant] of Object.entries(grants)) {
720
+ if (grant.tools === "*")
721
+ continue;
722
+ // The prefix check runs against the connected-service slugs, so an
723
+ // underscored service (bland_ai) accepts its own BLAND_AI_* names —
724
+ // and a plain-prefix grant cannot capture them while the real service
725
+ // is connected.
726
+ const misplaced = grant.tools.filter((tool) => (serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool)) !== slug);
727
+ if (misplaced.length) {
728
+ return `connectorTools.${slug}.tools names tools that belong to another service: ${misplaced.join(", ")}`;
729
+ }
730
+ }
731
+ if (!connected)
732
+ return null;
733
+ const unconnected = Object.keys(grants).filter((slug) => !connected[slug]?.connected);
734
+ if (unconnected.length) {
735
+ return `connectorTools names services that are not connected: ${unconnected.join(", ")}`;
736
+ }
737
+ const catalog = await listToolkits(cfg);
738
+ // source "curated" means the marketplace was unreachable, and a walk that
739
+ // never reached its natural end served only part of it — either way there
740
+ // is nothing trustworthy to cross-check against, and a grant for a service
741
+ // on a page the walk never saw must not be rejected.
742
+ if (catalog.source !== "api" || !catalog.pagination?.complete)
743
+ return null;
744
+ const known = new Set(catalog.cards.map((card) => card.slug.toLowerCase()));
745
+ const unknown = Object.keys(grants).filter((slug) => !known.has(slug));
746
+ if (unknown.length) {
747
+ return `connectorTools names services missing from the connected-apps catalog: ${unknown.join(", ")}`;
748
+ }
749
+ return null;
750
+ }
751
+ /** The grant editor's tool inventory, grouped by service slug. Sourced from
752
+ * the same MCP endpoint the bots' bridge relays to (initialize + tools/list
753
+ * through relayMcp), so what the picker offers is exactly what a granted bot
754
+ * would see — in self-hosted and managed modes alike. Platform meta-tools
755
+ * and per-service connection flows are not per-tool grants (they ride along
756
+ * whenever a service is granted), so they never appear in the picker. */
757
+ export async function listConnectorTools(cfg, options = {}) {
758
+ const identity = connectorToolsIdentity(cfg);
759
+ if (!options.force && connectorToolsCache?.identity === identity
760
+ && Date.now() - connectorToolsCache.at < CONNECTOR_TOOLS_CACHE_MS) {
761
+ return connectorToolsCache.services;
762
+ }
763
+ // Concurrent misses share one MCP walk — two editors opening together
764
+ // must not run the initialize + tools/list sequences twice. force
765
+ // bypasses the settled cache but still joins a walk already running.
766
+ if (connectorToolsRequest?.identity === identity)
767
+ return connectorToolsRequest.services;
768
+ const walk = collectConnectorTools(cfg).then((services) => {
769
+ connectorToolsCache = { at: Date.now(), identity, services };
770
+ return services;
771
+ });
772
+ connectorToolsRequest = { identity, services: walk };
773
+ try {
774
+ return await walk;
775
+ }
776
+ finally {
777
+ if (connectorToolsRequest?.services === walk)
778
+ connectorToolsRequest = null;
779
+ }
780
+ }
781
+ const CONNECTOR_TOOLS_CACHE_MS = 60_000;
782
+ let connectorToolsCache = null;
783
+ /** Which backend an inventory walk belongs to, mirroring the relay's own
784
+ * credential fingerprint: a project key or the managed broker. Cached
785
+ * inventories are keyed by it, so a config switch can never serve the
786
+ * previous backend's tools for another cache window. */
787
+ function connectorToolsIdentity(cfg) {
788
+ const apiKey = projectApiKey(cfg);
789
+ if (apiKey)
790
+ return backendFingerprint("project-tools", apiBase(), apiKey);
791
+ const broker = brokerAccess();
792
+ return broker ? backendFingerprint("managed-tools", broker.url, broker.token) : "none";
793
+ }
794
+ let connectorToolsRequest = null;
795
+ /** The service slugs grant enforcement resolves tool-name prefixes
796
+ * against: every key of the connection inventory, connected or not,
797
+ * because the real backend slugs are what separates an underscored
798
+ * service (bland_ai) from a plain-prefix grant (bland). Cached like the
799
+ * tool inventory — 60s, keyed by backend identity, one shared in-flight
800
+ * walk — and an unreachable inventory reads as an empty list without
801
+ * caching the failure, so callers fall back to the plain split and the
802
+ * next call retries. */
803
+ export async function connectedServiceSlugs(cfg) {
804
+ const identity = connectorToolsIdentity(cfg);
805
+ if (connectorServiceSlugsCache?.identity === identity
806
+ && Date.now() - connectorServiceSlugsCache.at < CONNECTOR_SERVICE_SLUGS_CACHE_MS) {
807
+ return connectorServiceSlugsCache.slugs;
808
+ }
809
+ if (connectorServiceSlugsRequest?.identity === identity)
810
+ return connectorServiceSlugsRequest.slugs;
811
+ const walk = connectedServices(cfg).then((services) => {
812
+ const slugs = Object.keys(services);
813
+ connectorServiceSlugsCache = { at: Date.now(), identity, slugs };
814
+ return slugs;
815
+ }, () => []);
816
+ connectorServiceSlugsRequest = { identity, slugs: walk };
817
+ try {
818
+ return await walk;
819
+ }
820
+ finally {
821
+ if (connectorServiceSlugsRequest?.slugs === walk)
822
+ connectorServiceSlugsRequest = null;
823
+ }
824
+ }
825
+ const CONNECTOR_SERVICE_SLUGS_CACHE_MS = 60_000;
826
+ let connectorServiceSlugsCache = null;
827
+ let connectorServiceSlugsRequest = null;
828
+ /** Tool names longer than any Composio description worth searching. */
829
+ const TOOL_DESCRIPTION_MAX = 240;
830
+ const TOOLS_LIST_PAGES_MAX = 10;
831
+ function connectorToolListing(tool) {
832
+ if (!tool || typeof tool !== "object" || Array.isArray(tool))
833
+ return null;
834
+ const name = tool.name;
835
+ if (typeof name !== "string" || !CONNECTOR_TOOL_NAME_PATTERN.test(name))
836
+ return null;
837
+ if (name.startsWith("COMPOSIO_"))
838
+ return null;
839
+ if (name.endsWith("_MANAGE_CONNECTIONS") || name.endsWith("_WAIT_FOR_CONNECTIONS"))
840
+ return null;
841
+ const description = tool.description;
842
+ const trimmed = typeof description === "string"
843
+ ? description.replace(/\s+/g, " ").trim().slice(0, TOOL_DESCRIPTION_MAX)
844
+ : "";
845
+ return trimmed ? { name, description: trimmed } : { name };
846
+ }
847
+ /** One JSON-RPC response frame from a JSON or SSE body, mirroring the
848
+ * bridge's parseUpstream: SSE data lines are tried in order and the frame
849
+ * carrying the request id wins. */
850
+ function parseMcpResponse(text, id) {
851
+ const trimmed = text.trim();
852
+ if (!trimmed)
853
+ return null;
854
+ const candidate = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
855
+ if (trimmed.startsWith("{")) {
856
+ const parsed = JSON.parse(trimmed);
857
+ return candidate(parsed) ? parsed : null;
858
+ }
859
+ const frames = trimmed
860
+ .split(/\r?\n/)
861
+ .filter((line) => line.startsWith("data:"))
862
+ .map((line) => line.slice(5).trim())
863
+ .filter((line) => line && line !== "[DONE]")
864
+ .flatMap((line) => {
865
+ try {
866
+ return [JSON.parse(line)];
867
+ }
868
+ catch {
869
+ return [];
870
+ }
871
+ })
872
+ .filter(candidate);
873
+ return frames.findLast((frame) => frame.id === id) ?? frames.at(-1) ?? null;
874
+ }
875
+ async function collectConnectorTools(cfg) {
876
+ // Connected services are the real slugs, so they resolve underscored
877
+ // services (bland_ai) the plain split would file under "bland"; an
878
+ // unreachable inventory degrades to that split.
879
+ const connected = await connectedServices(cfg).catch(() => null);
880
+ const candidates = connected ? Object.keys(connected) : [];
881
+ const initialize = await relayMcp(cfg, {
882
+ jsonrpc: "2.0",
883
+ id: "omb-grants-initialize",
884
+ method: "initialize",
885
+ params: {
886
+ protocolVersion: "2025-03-26",
887
+ capabilities: {},
888
+ clientInfo: { name: "openmausbot-grant-editor", version: "1" },
889
+ },
890
+ });
891
+ if (initialize.status !== 200) {
892
+ throw new Error(await responseErrorFromBytes(initialize.status, initialize.bytes));
893
+ }
894
+ const transportSessionId = initialize.transportSessionId;
895
+ // Streamable-HTTP servers expect the handshake notification before the
896
+ // first request; it carries no response, so failures are not fatal.
897
+ await relayMcp(cfg, {
898
+ jsonrpc: "2.0",
899
+ method: "notifications/initialized",
900
+ }, transportSessionId).catch(() => undefined);
901
+ const grouped = new Map();
902
+ let cursor;
903
+ for (let page = 0; page < TOOLS_LIST_PAGES_MAX; page += 1) {
904
+ const list = await relayMcp(cfg, {
905
+ jsonrpc: "2.0",
906
+ id: "omb-grants-tools",
907
+ method: "tools/list",
908
+ params: cursor ? { cursor } : {},
909
+ }, transportSessionId);
910
+ if (list.status !== 200) {
911
+ throw new Error(await responseErrorFromBytes(list.status, list.bytes));
912
+ }
913
+ const frame = parseMcpResponse(new TextDecoder().decode(list.bytes), "omb-grants-tools");
914
+ const error = frame && typeof frame.error === "object" && frame.error !== null
915
+ ? frame.error.message
916
+ : undefined;
917
+ if (typeof error === "string" && error)
918
+ throw new Error(error);
919
+ const result = frame && typeof frame.result === "object" && frame.result !== null
920
+ ? frame.result
921
+ : undefined;
922
+ if (!result || !Array.isArray(result.tools)) {
923
+ throw new Error("Composio returned a tools/list response without a tools array");
924
+ }
925
+ for (const tool of result.tools) {
926
+ const listing = connectorToolListing(tool);
927
+ if (!listing)
928
+ continue;
929
+ const slug = serviceSlugForCandidates(listing.name, candidates) ?? serviceSlugFor(listing.name);
930
+ if (!slug)
931
+ continue;
932
+ const bucket = grouped.get(slug) ?? [];
933
+ if (!bucket.some((existing) => existing.name === listing.name))
934
+ bucket.push(listing);
935
+ grouped.set(slug, bucket);
936
+ }
937
+ cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
938
+ if (!cursor)
939
+ break;
940
+ }
941
+ for (const bucket of grouped.values())
942
+ bucket.sort((a, b) => a.name.localeCompare(b.name));
943
+ return Object.fromEntries([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)));
944
+ }
945
+ async function responseErrorFromBytes(status, bytes) {
946
+ try {
947
+ const body = JSON.parse(new TextDecoder().decode(bytes));
948
+ const message = body && typeof body === "object" && !Array.isArray(body)
949
+ ? body.error
950
+ : undefined;
951
+ if (message && typeof message === "object" && !Array.isArray(message)) {
952
+ const text = message.message;
953
+ if (typeof text === "string" && text)
954
+ return text;
955
+ }
956
+ if (typeof message === "string" && message)
957
+ return message;
958
+ }
959
+ catch {
960
+ // fall through to the generic status line
961
+ }
962
+ return `Composio tools/list: HTTP ${status}`;
963
+ }
675
964
  export async function connectionStatus(cfg, slugs) {
676
965
  const canonicalSlugs = [...new Set(slugs.map(canonicalToolkitSlug))];
677
966
  const apiKey = projectApiKey(cfg);
@@ -989,9 +1278,16 @@ export async function listToolkits(cfg) {
989
1278
  const pagingStoppedEarly = lastReportedPage !== undefined
990
1279
  && reportedTotalPages !== undefined
991
1280
  && lastReportedPage < reportedTotalPages;
992
- const pagination = { items: uniqueCards.length, stalled: shortOfReportedTotal || pagingStoppedEarly };
1281
+ const stalled = shortOfReportedTotal || pagingStoppedEarly;
1282
+ // "end" and "total-reached" are the walk's natural finishes; every
1283
+ // other stop reason left pages unread even when the API reports no
1284
+ // totals for stalled to compare against.
1285
+ const complete = (stop === "end" || stop === "total-reached") && !stalled;
1286
+ const pagination = { items: uniqueCards.length, stalled, complete };
993
1287
  if (reportedTotalItems !== undefined)
994
1288
  pagination.totalItems = reportedTotalItems;
1289
+ if (pagination.stalled)
1290
+ pagination.reason = stop;
995
1291
  if (pagination.stalled) {
996
1292
  console.warn(`[composio] marketplace catalog paging stopped early (${stop}) after ${uniqueCards.length} toolkits`
997
1293
  + (reportedTotalItems !== undefined ? ` of ~${reportedTotalItems}` : "")
@@ -7,8 +7,17 @@
7
7
  // that, name the holder as a bot running a thread, and keep the same words
8
8
  // on every surface (chat chip, room chip, the gate's refusal) so a person
9
9
  // who has read it once recognises it everywhere.
10
- /** Whole minutes, or seconds under one minute, for the give-up notice. */
11
- const minutes = (ms) => ms >= 60_000 ? `${Math.round(ms / 60_000)} minutes` : `${Math.round(ms / 1000)} seconds`;
10
+ /** Whole minutes, or seconds under one minute, singular at exactly one. */
11
+ const minutes = (ms) => {
12
+ const value = ms >= 60_000 ? Math.round(ms / 60_000) : Math.round(ms / 1000);
13
+ const unit = ms >= 60_000 ? "minute" : "second";
14
+ return `${value} ${unit}${value === 1 ? "" : "s"}`;
15
+ };
16
+ /** How long a wait lasted, for the resolution line: waits under a second
17
+ * are honest about being over in a blink instead of claiming "0 seconds". */
18
+ export function computerWaitDuration(ms) {
19
+ return ms < 1_000 ? "under a second" : minutes(ms);
20
+ }
12
21
  const holderPhrase = (holder) => holder.task ? `${holder.name} is running ${holder.task}` : `${holder.name} is using it`;
13
22
  /** The chip a turn shows while it is queued behind another turn's desktop. */
14
23
  export function computerWaitingText(holder) {
@@ -16,14 +25,18 @@ export function computerWaitingText(holder) {
16
25
  return "Waiting for its turn on this computer. Starts automatically when it is free.";
17
26
  return `Waiting for its turn on this computer — ${holderPhrase(holder)}. Starts automatically when that finishes.`;
18
27
  }
19
- /** The same chip once the wait landed and this turn holds the desktop. */
20
- export function computerFreeText() {
21
- return "Computer free — continuing";
28
+ /** The resolution appended once the wait landed and this turn holds the
29
+ * desktop: how long it waited, and who held it. The waiting chip stays as
30
+ * written — this line is the history beside it, not its replacement. */
31
+ export function computerFreeAfterText(holder, waitedMs) {
32
+ const who = holder ? ` (${holder.task ? `${holder.name} · ${holder.task}` : holder.name} held it)` : "";
33
+ return `Computer free — continuing after waiting ${computerWaitDuration(waitedMs)}${who}`;
22
34
  }
23
- /** The same chip when the wait ended without the desktop: the turn was
24
- * stopped, or the claim was abandoned. */
25
- export function computerWaitEndedText() {
26
- return "Stopped waiting for the computer";
35
+ /** The resolution appended when the wait ended because the turn was stopped
36
+ * or its claim was cancelled. */
37
+ export function computerStoppedWaitingText(holder, waitedMs) {
38
+ const who = holder ? ` — ${holderPhrase(holder)}` : "";
39
+ return `Stopped waiting for the computer after ${computerWaitDuration(waitedMs)}${who}.`;
27
40
  }
28
41
  /** The error after the wait ceiling: still names who holds it, and says
29
42
  * what a person can do — stop that turn, or move this one. */
@@ -7,6 +7,7 @@ import { join } from "node:path";
7
7
  import { z } from "zod";
8
8
  import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
9
9
  import { writeFileAtomic } from "./atomic.js";
10
+ import { newBotDefaultsSchema } from "./new-bot-defaults.js";
10
11
  import { EFFORT_LEVELS } from "../shared/wire.js";
11
12
  import { isModelVariant } from "./contracts.js";
12
13
  import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
@@ -32,7 +33,7 @@ export const MAX_THREAD_EVENT_LOG_BYTES = 4 * 1024 * 1024 * 1024;
32
33
  export const DEFAULT_LOCAL_VM_MODE = "shared";
33
34
  export const DEFAULT_LOCAL_VM_MAX_INSTANCES = 2;
34
35
  export const MIN_LOCAL_VM_MAX_INSTANCES = 1;
35
- export const MAX_LOCAL_VM_MAX_INSTANCES = 4;
36
+ export const MAX_LOCAL_VM_MAX_INSTANCES = 8;
36
37
  export function isValidSshAlias(value) {
37
38
  return typeof value === "string" && SSH_ALIAS.test(value);
38
39
  }
@@ -289,8 +290,8 @@ const instanceConfigSchema = z.object({
289
290
  });
290
291
  const instanceConfigMapSchema = z.record(z.string(), instanceConfigSchema);
291
292
  const defaultModelSelectionSchema = z.object({
292
- instanceId: z.string().trim().min(1),
293
- model: z.string().trim().min(1),
293
+ instanceId: z.string().trim().min(1).max(200),
294
+ model: z.string().trim().min(1).max(500),
294
295
  effort: z.enum(EFFORT_LEVELS).optional(),
295
296
  variant: z.string().refine(isModelVariant, "invalid model variant").optional(),
296
297
  }).refine((selection) => selection.variant === undefined || selection.effort === undefined, "choose either a model variant or an effort level");
@@ -303,6 +304,15 @@ const threadsConfigSchema = z.object({
303
304
  * Absent keeps them forever. */
304
305
  eventLogRetentionDays: z.number().int().min(1).max(3650).optional(),
305
306
  }).strict();
307
+ /** Workspace-wide defaults every new bot starts with (Store.createBot). */
308
+ const newBotsConfigSchema = z.object({
309
+ /** Effort for a new bot whose model selection names none. */
310
+ effort: z.enum(EFFORT_LEVELS).optional(),
311
+ }).strict();
312
+ /** PATCH newBots: null clears a default back to absent (no level is sent). */
313
+ const newBotsPatchSchema = z.object({
314
+ effort: newBotsConfigSchema.shape.effort.nullable(),
315
+ }).strict();
306
316
  /** PATCH threads: every knob is independently patchable, and null clears an
307
317
  * event-log knob back to its absent (off) default. */
308
318
  const threadsPatchSchema = threadsConfigSchema.extend({
@@ -317,12 +327,15 @@ const appConfigSchema = z.object({
317
327
  * addresses or `@domain` entries; admins get every scope, members chat only. */
318
328
  signIn: z.object({ admins: z.array(z.string().max(320)).max(500).optional(), members: z.array(z.string().max(320)).max(5000).optional() }).optional(),
319
329
  defaultModelSelection: defaultModelSelectionSchema.optional(),
330
+ newBotDefaults: newBotDefaultsSchema.optional(),
331
+ newBots: newBotsConfigSchema.optional(),
320
332
  /** CLI-only launch preferences. Never enable remote access implicitly. */
321
333
  cliStartup: z.object({
322
334
  access: z.enum(["local", "tunnel", "tailscale", "public-url"]),
323
335
  publicUrl: z.string().url().optional(),
324
336
  phone: z.enum(["ios", "android"]).optional(),
325
337
  }).optional(),
338
+ mistral: z.object({ key: optionalText }).optional(),
326
339
  xai: z.object({ key: optionalText, url: optionalText }).optional(),
327
340
  /** Anthropic API key for Claude Code billed per token, handed only to
328
341
  * Claude instances; `url` only for a proxy or a test double. Never a
@@ -400,8 +413,8 @@ const appConfigSchema = z.object({
400
413
  }).optional(),
401
414
  customModel: z.string().trim().max(200).refine((value) => !["\r", "\n", "\0"].some((character) => value.includes(character)), "Use a model ID without control characters").optional(),
402
415
  }).optional(),
403
- /** Non-secret profile details shown in the sidebar. */
404
- profile: z.object({ name: optionalText, email: optionalText }).optional(),
416
+ /** Non-secret profile details; aboutMe is shared with every bot. */
417
+ profile: z.object({ name: optionalText, email: optionalText, aboutMe: z.string().max(24_000).optional() }).optional(),
405
418
  /** UI language override (BCP-47, lowercase). Empty/absent = follow the
406
419
  * system language. Unknown tags degrade to English in the renderer. */
407
420
  language: optionalText,
@@ -432,7 +445,7 @@ const storedAppConfigSchema = appConfigSchema.extend({
432
445
  browserProfiles: storedBrowserProfilesSchema.optional(),
433
446
  });
434
447
  const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
435
- .extend({ threads: threadsPatchSchema.optional() });
448
+ .extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
436
449
  const jsonObjectSchema = z.record(z.string(), z.json());
437
450
  /** Resolve a canonical profile record to its exact durable Electron
438
451
  * partition identity. Callers must never substitute the display/API id. */
@@ -670,13 +683,35 @@ function migrateLegacyFeatureFlags() {
670
683
  console.error(`[config] could not rename features.skillRecorder: ${error instanceof Error ? error.message : String(error)}`);
671
684
  }
672
685
  }
686
+ /** The last "config.json is being ignored" warning, so a file that stays
687
+ * broken is reported once rather than on every loadConfig() call. */
688
+ let lastIgnoredConfigWarning = "";
673
689
  export function loadConfig() {
674
690
  let cfg = {};
675
691
  try {
676
692
  cfg = parseStoredConfig(parseJson(readFileSync(join(DATA_DIR, "config.json"), "utf8")));
693
+ lastIgnoredConfigWarning = "";
677
694
  }
678
- catch {
679
- /* first run — env fallbacks below */
695
+ catch (error) {
696
+ // No file yet is a normal first run: env fallbacks below. Any other failure
697
+ // (unreadable file, invalid JSON, a schema error in one field) means the
698
+ // whole file is being ignored for this process — every instance, account
699
+ // and setting in it — so say why instead of silently running on defaults.
700
+ // saveConfig() still merges into the raw file, so nothing on disk is lost;
701
+ // the user just needs to know which field to fix.
702
+ if (error?.code !== "ENOENT") {
703
+ // JSON.parse includes a fragment of the input in some error messages.
704
+ // A malformed credential must never be copied into the server log.
705
+ const reason = error instanceof SyntaxError ? "invalid JSON"
706
+ : error instanceof Error ? error.message : "unable to read configuration";
707
+ const warning = `config: ignoring ${join(DATA_DIR, "config.json")} and using defaults: ${reason}`;
708
+ if (warning !== lastIgnoredConfigWarning)
709
+ console.warn(warning);
710
+ lastIgnoredConfigWarning = warning;
711
+ }
712
+ else {
713
+ lastIgnoredConfigWarning = "";
714
+ }
680
715
  }
681
716
  // Env wins over the file for every credential. The desktop shell keeps
682
717
  // these secrets OS-encrypted and hands them to this process as env at
@@ -685,6 +720,9 @@ export function loadConfig() {
685
720
  // Anything that saves a credential mid-session must keep process.env in
686
721
  // step (syncCredentialEnv below), or the value injected at boot would
687
722
  // shadow the save until the next launch.
723
+ cfg.mistral = { ...cfg.mistral };
724
+ if (process.env.MISTRAL_API_KEY !== undefined)
725
+ cfg.mistral.key = process.env.MISTRAL_API_KEY;
688
726
  cfg.xai = { ...cfg.xai };
689
727
  if (process.env.XAI_API_KEY !== undefined)
690
728
  cfg.xai.key = process.env.XAI_API_KEY;
@@ -746,6 +784,7 @@ export function loadConfig() {
746
784
  export function syncCredentialEnv(patch) {
747
785
  const secrets = [
748
786
  [patch.xai?.key, "XAI_API_KEY"],
787
+ [patch.mistral?.key, "MISTRAL_API_KEY"],
749
788
  [patch.anthropic?.key, "OMB_ANTHROPIC_API_KEY"],
750
789
  [patch.openaiCompat?.key, "OPENAI_COMPAT_API_KEY"],
751
790
  [patch.composio?.apiKey, "COMPOSIO_API_KEY"],
@@ -788,6 +827,7 @@ export function syncCredentialEnv(patch) {
788
827
  * child these are someone else's keys riding along in `...process.env`. */
789
828
  export const WORKSPACE_CREDENTIAL_ENV = [
790
829
  "XAI_API_KEY",
830
+ "MISTRAL_API_KEY",
791
831
  "OMB_ANTHROPIC_API_KEY",
792
832
  "OMB_ANTHROPIC_API_URL",
793
833
  "OMB_HOSTED_MODEL_TOKEN",
@@ -850,6 +890,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
850
890
  "OPENAI_API_KEY",
851
891
  "OPENCODE_API_KEY",
852
892
  "XAI_API_KEY",
893
+ "MISTRAL_API_KEY",
853
894
  "CURSOR_API_KEY",
854
895
  "CURSOR_AUTH_TOKEN",
855
896
  ];
@@ -874,7 +915,7 @@ export function saveConfig(patch, options = {}) {
874
915
  catch {
875
916
  /* first write */
876
917
  }
877
- const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional() }).parse(patch);
918
+ const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() }).parse(patch);
878
919
  const before = configSaveListeners.size ? structuredClone(disk) : disk;
879
920
  // A write is the durable migration point. Preserve every other raw key in
880
921
  // config.json, but never write #567's mixed-case or duplicate profile ids
@@ -882,7 +923,7 @@ export function saveConfig(patch, options = {}) {
882
923
  const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
883
924
  if (storedProfiles.success)
884
925
  disk.browserProfiles = storedProfiles.data;
885
- for (const key of ["xai", "anthropic", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine"]) {
926
+ for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
886
927
  const section = checkedPatch[key];
887
928
  if (!section)
888
929
  continue;
@@ -894,8 +935,9 @@ export function saveConfig(patch, options = {}) {
894
935
  if (key === "threads" && !current.success)
895
936
  merged.maxConcurrentPerBot = DEFAULT_MAX_CONCURRENT_BOT_THREADS;
896
937
  Object.assign(merged, section);
897
- // null is the patch's explicit "remove this key" marker (today only the
898
- // threads event-log knobs use it); a key the patch omits keeps its value.
938
+ // null is the patch's explicit "remove this key" marker (the threads
939
+ // event-log knobs and newBots.effort use it); a key the patch omits
940
+ // keeps its value.
899
941
  for (const [sectionKey, sectionValue] of Object.entries(section)) {
900
942
  if (sectionValue === null)
901
943
  delete merged[sectionKey];
@@ -915,6 +957,24 @@ export function saveConfig(patch, options = {}) {
915
957
  // an effort level omitted from the new selection.
916
958
  if (checkedPatch.defaultModelSelection !== undefined) {
917
959
  disk.defaultModelSelection = checkedPatch.defaultModelSelection;
960
+ if (disk.newBotDefaults) {
961
+ const previousDefaults = newBotDefaultsSchema.parse(disk.newBotDefaults);
962
+ disk.newBotDefaults = newBotDefaultsSchema.parse({
963
+ ...previousDefaults,
964
+ profile: { ...previousDefaults.profile, modelSelection: checkedPatch.defaultModelSelection },
965
+ });
966
+ }
967
+ }
968
+ // Replace the complete template so clearing a field, file or routine
969
+ // cannot revive an old value through the general section merge above.
970
+ if (checkedPatch.newBotDefaults !== undefined) {
971
+ disk.newBotDefaults = checkedPatch.newBotDefaults;
972
+ if (checkedPatch.newBotDefaults.profile.modelSelection) {
973
+ disk.defaultModelSelection = checkedPatch.newBotDefaults.profile.modelSelection;
974
+ }
975
+ else {
976
+ delete disk.defaultModelSelection;
977
+ }
918
978
  }
919
979
  if (checkedPatch.cliStartup !== undefined)
920
980
  disk.cliStartup = checkedPatch.cliStartup;
@@ -938,6 +998,14 @@ export function saveConfig(patch, options = {}) {
938
998
  if (routingConflict)
939
999
  throw Object.assign(new Error(routingConflict), { status: 409 });
940
1000
  disk.browserProfiles = nextProfiles;
1001
+ if (disk.newBotDefaults) {
1002
+ const defaults = newBotDefaultsSchema.parse(disk.newBotDefaults);
1003
+ const profileId = defaults.profile.browserProfile;
1004
+ if (profileId && profileId !== "guest" && !nextProfiles.some(profile => profile.id === profileId)) {
1005
+ delete defaults.profile.browserProfile;
1006
+ disk.newBotDefaults = defaults;
1007
+ }
1008
+ }
941
1009
  }
942
1010
  if (checkedPatch.instances) {
943
1011
  const currentInstances = jsonObjectSchema.safeParse(disk.instances);
@@ -1046,6 +1114,8 @@ export function persistableInstanceConfigs(cfg) {
1046
1114
  * environment of an unrelated child process. */
1047
1115
  function injectedEnvironment(cfg, driver) {
1048
1116
  const environment = new Map();
1117
+ if (driver === "mistral" && cfg.mistral?.key)
1118
+ environment.set("MISTRAL_API_KEY", cfg.mistral.key);
1049
1119
  if (driver === "grok" && cfg.xai?.key)
1050
1120
  environment.set("XAI_API_KEY", cfg.xai.key);
1051
1121
  // The workspace Anthropic key reaches Claude Code as the variable it
@@ -1096,6 +1166,7 @@ export function instanceConfigs(cfg) {
1096
1166
  opencodeGo: { driver: "opencodeGo" },
1097
1167
  computer: { driver: "boxAgent" },
1098
1168
  openaiCompat: { driver: "openai-compat" },
1169
+ mistral: { driver: "mistral" },
1099
1170
  qwen: { driver: "qwenAgent" },
1100
1171
  hermes: { driver: "hermesAgent" },
1101
1172
  pi: { driver: "piAgent" },
@@ -1111,6 +1182,7 @@ export function instanceConfigs(cfg) {
1111
1182
  const PRODUCT_FLEET_ADDITIONS = {
1112
1183
  cursor: { driver: "cursorAgent" },
1113
1184
  openaiCompat: { driver: "openai-compat" },
1185
+ mistral: { driver: "mistral" },
1114
1186
  ...CUSTOM_ONLY,
1115
1187
  };
1116
1188
  const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;