openmausbot 0.1.91 → 0.1.92

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 (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -10,10 +10,16 @@ import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
10
10
  //
11
11
  // resumeCursor is the codex thread id; a later turn tries thread/resume
12
12
  // and preserves that history or reports a failed resume.
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, mkdirSync } from "node:fs";
14
+ import { createHash } from "node:crypto";
15
+ import { join } from "node:path";
14
16
  import { homedir } from "node:os";
15
17
  import { codexConfigMcpServerNames, mountedMcpServerName } from "./codex-mcp-names.js";
16
- import { stripWorkspaceCredentialEnv } from "../config.js";
18
+ import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
19
+ import { hostedWorkspaceConfigured } from "../enterprise.js";
20
+ import { cloudHomeConfigured } from "../cloud-home.js";
21
+ import { serverVersion } from "../environment.js";
22
+ import { ChatGptPlanAuthController } from "./chatgpt-plan-auth.js";
17
23
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
18
24
  import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
19
25
  import { newEventId, newId } from "../contracts.js";
@@ -34,6 +40,17 @@ import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from
34
40
  import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
35
41
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
36
42
  const DRIVER_KIND = "codex";
43
+ export function codexUserError(value, chatgptPlan) {
44
+ if (chatgptPlan && value.includes("subscription_sharing_usage_limit_exceeded")) {
45
+ return "subscription_sharing_usage_limit_exceeded: Your ChatGPT plan usage limit was reached. Manage usage in ChatGPT Settings, or explicitly choose another provider.";
46
+ }
47
+ if (value.includes("provider_not_configured")) {
48
+ return chatgptPlan
49
+ ? "provider_not_configured: ChatGPT has not enabled this model for the selected account. Refresh models or reconnect ChatGPT plan in Settings. API billing will not be used."
50
+ : "provider_not_configured: This Codex account cannot use the selected route. For the new ChatGPT plan models, choose ChatGPT plan in Settings → Engines and Continue with ChatGPT, then select a model from that account.";
51
+ }
52
+ return value.slice(0, 400);
53
+ }
37
54
  class CodexRpcError extends Error {
38
55
  code;
39
56
  constructor(error) {
@@ -79,12 +96,32 @@ async function codexReleaseUpdate(version, cli) {
79
96
  }
80
97
  function decodeConfig(raw) {
81
98
  const o = (raw ?? {});
99
+ if (o.authMode !== undefined && o.authMode !== "chatgpt-plan")
100
+ throw new Error("Unknown Codex sign-in mode.");
101
+ if (o.authMode && o.managed)
102
+ throw new Error("ChatGPT plan and Company billing cannot be combined.");
82
103
  return {
83
104
  cli: typeof o.cli === "string" ? o.cli : "codex",
84
105
  fullAuto: o.fullAuto === true,
106
+ ...(o.authMode === "chatgpt-plan" ? { authMode: "chatgpt-plan" } : {}),
85
107
  ...(o.managed && typeof o.managed === "object" ? { managed: decodeManagedCodex(o.managed) } : {}),
86
108
  };
87
109
  }
110
+ export function chatgptPlanCodexArgs() {
111
+ return [
112
+ "-c", 'model_provider="openai_chatgpt_plan"',
113
+ "-c", 'model_providers.openai_chatgpt_plan.name="ChatGPT plan"',
114
+ "-c", 'model_providers.openai_chatgpt_plan.base_url="https://api.openai.com/v1"',
115
+ "-c", 'model_providers.openai_chatgpt_plan.env_key="OPENMAUSBOT_CHATGPT_TOKEN"',
116
+ "-c", 'model_providers.openai_chatgpt_plan.wire_api="responses"',
117
+ "-c", "model_providers.openai_chatgpt_plan.requires_openai_auth=false",
118
+ "-c", "model_providers.openai_chatgpt_plan.supports_websockets=false",
119
+ "-c", 'cli_auth_credentials_store="ephemeral"',
120
+ "-c", "features.tool_search=false",
121
+ "-c", "shell_environment_policy.ignore_default_excludes=false",
122
+ "-c", 'shell_environment_policy.exclude=["OPENMAUSBOT_CHATGPT_TOKEN"]',
123
+ ];
124
+ }
88
125
  function decodeManagedCodex(raw) {
89
126
  const value = raw;
90
127
  if (typeof value.url !== "string" || !Array.isArray(value.models) || !value.models.length || value.models.some(model => typeof model !== "string" || !/^[\w][\w./+-]*$/.test(model))) {
@@ -226,6 +263,16 @@ function mcpAppApprovalForm(params) {
226
263
  const tool = boundedLabel(appName) ?? boundedLabel(request.serverName) ?? "app_access";
227
264
  return { tool, summary: message, allowResult: { action: "accept", content } };
228
265
  }
266
+ /** What a guest-driven turn's app-server starts with: the shell, unified
267
+ * exec and image reads off (codex-cli 0.159.0 `features`), web search too. */
268
+ export const GUEST_CONFINED_CODEX_ARGS = [
269
+ "-c", "features.shell_tool=false", "-c", "features.unified_exec=false", "-c", "features.view_image=false", "-c", 'web_search="disabled"',
270
+ ];
271
+ /** Whether the effective config (config/read) shows the shell turned off. */
272
+ export function codexShellDisabled(config) {
273
+ const features = plainRecord(plainRecord(config)?.features);
274
+ return features?.shell_tool === false && features?.unified_exec === false && features?.view_image === false;
275
+ }
229
276
  /** Codex persists these values on its native thread. Keep them explicit on
230
277
  * start, resume, and every turn so switching modes cannot leave a more
231
278
  * permissive sandbox/reviewer stuck to the next request. */
@@ -492,6 +539,12 @@ export const CodexDriver = {
492
539
  defaultConfig: () => decodeConfig({}),
493
540
  async create(input) {
494
541
  const { instanceId, config } = input;
542
+ const plan = config.authMode === "chatgpt-plan";
543
+ const planUnavailable = plan && (hostedWorkspaceConfigured() || cloudHomeConfigured())
544
+ ? "ChatGPT plan sign-in for hosted Pro requires OpenAI's hosted-app approval. Use a Company model or API key until that integration is approved; the local desktop flow cannot be used here."
545
+ : undefined;
546
+ // Provider homes are excluded from portable backups and Move to Cloud.
547
+ const planDirectory = join(DATA_DIR, "providers", "chatgpt-plan", createHash("sha256").update(instanceId).digest("hex"));
495
548
  const childEnv = () => {
496
549
  const env = {
497
550
  ...process.env,
@@ -505,13 +558,31 @@ export const CodexDriver = {
505
558
  // The harness process may hold workspace credentials (xai/box/voice
506
559
  // keys, env-injected at boot); none of them are this CLI's to see.
507
560
  stripWorkspaceCredentialEnv(env);
561
+ delete env.OPENMAUSBOT_CHATGPT_TOKEN;
562
+ if (plan)
563
+ env.CODEX_HOME = join(planDirectory, "codex");
508
564
  return env;
509
565
  };
510
566
  const catalogEnv = childEnv();
511
- let models = config.managed ? { default: config.managed.models[0], options: config.managed.models.map(id => ({ id, label: id })) } : STATIC_CODEX_MODELS;
567
+ const planAuth = plan ? new ChatGptPlanAuthController({ directory: planDirectory }) : null;
568
+ let planGeneration = 0;
569
+ let planSigningOut = false;
570
+ let disposed = false;
571
+ let planWarning;
572
+ let models = plan ? { default: "", options: [] } : config.managed ? { default: config.managed.models[0], options: config.managed.models.map(id => ({ id, label: id })) } : STATIC_CODEX_MODELS;
512
573
  const refreshModels = async () => {
513
574
  if (config.managed)
514
575
  return;
576
+ if (planAuth) {
577
+ const generation = planGeneration;
578
+ models = { default: "", options: [] };
579
+ if (!planUnavailable && !planSigningOut && !disposed) {
580
+ const catalog = await planAuth.models();
581
+ if (generation === planGeneration && !planSigningOut && !disposed)
582
+ models = catalog;
583
+ }
584
+ return;
585
+ }
515
586
  try {
516
587
  const resolved = await readCodexModelCatalog(catalogEnv, fetch, config.cli);
517
588
  if (resolved.options.length)
@@ -521,7 +592,16 @@ export const CodexDriver = {
521
592
  // Keep the last usable catalog when a local provider is down.
522
593
  }
523
594
  };
524
- await refreshModels();
595
+ // A revoked grant or a temporary catalog outage must leave the account
596
+ // reachable in Settings for reconnect; it must not become a shadow.
597
+ if (planAuth) {
598
+ try {
599
+ await refreshModels();
600
+ }
601
+ catch { /* Explicit refresh reports the error. */ }
602
+ }
603
+ else
604
+ await refreshModels();
525
605
  const authentication = new CodexDeviceAuthController({
526
606
  cli: config.cli,
527
607
  environment: childEnv,
@@ -541,6 +621,27 @@ export const CodexDriver = {
541
621
  createdAt: new Date().toISOString(),
542
622
  });
543
623
  const sendTurn = async (turn) => {
624
+ const generation = planGeneration;
625
+ const assertPlanCurrent = () => {
626
+ if (disposed)
627
+ throw new Error("This provider was removed. Select a connected account and send again.");
628
+ if (planAuth && (planSigningOut || generation !== planGeneration))
629
+ throw new Error("The ChatGPT account changed while this message was preparing. Select a connected account and send again.");
630
+ };
631
+ assertPlanCurrent();
632
+ if (planUnavailable)
633
+ throw new Error(planUnavailable);
634
+ const planToken = planAuth ? await planAuth.accessToken() : undefined;
635
+ assertPlanCurrent();
636
+ if (planAuth) {
637
+ if (!turn.model || !models.options.some(model => model.id === turn.model)) {
638
+ await refreshModels();
639
+ assertPlanCurrent();
640
+ if (!turn.model || !models.options.some(model => model.id === turn.model))
641
+ throw new Error("This model is not available to the selected ChatGPT plan. Refresh models and choose a listed model; API billing will not be used.");
642
+ }
643
+ mkdirSync(join(planDirectory, "codex"), { recursive: true, mode: 0o700 });
644
+ }
544
645
  if (config.managed) {
545
646
  // One blanket refusal hides which prerequisite broke; name it so the
546
647
  // person can fix the actual gap instead of reconnecting blind.
@@ -587,7 +688,13 @@ export const CodexDriver = {
587
688
  const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
588
689
  const launchAttempt = async (attempt) => {
589
690
  const env = childEnv();
590
- const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs()];
691
+ if (planToken)
692
+ env.OPENMAUSBOT_CHATGPT_TOKEN = planToken;
693
+ const appServerArgs = ["app-server", ...(plan ? chatgptPlanCodexArgs() : config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs(),
694
+ // A guest-driven turn (SendTurnInput.guestConfined): no shell and
695
+ // no file reads, whatever the person's own config says (-c wins
696
+ // over config files). The turn also starts with no environment.
697
+ ...(turn.guestConfined ? GUEST_CONFINED_CODEX_ARGS : [])];
591
698
  if (turn.integrations?.composio) {
592
699
  mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
593
700
  }
@@ -746,8 +853,8 @@ export const CodexDriver = {
746
853
  while (!state.settled && Date.now() < deadline) {
747
854
  await new Promise((wake) => setTimeout(wake, 15));
748
855
  }
749
- if (state.settled)
750
- return true;
856
+ // Protocol completion is not process termination. In particular,
857
+ // sign-out must wait until the child holding its token has exited.
751
858
  }
752
859
  const stopped = await terminate();
753
860
  if (stopped)
@@ -960,6 +1067,24 @@ export const CodexDriver = {
960
1067
  (params.additionalPermissions != null || params.networkApprovalContext != null)) || undefined,
961
1068
  });
962
1069
  };
1070
+ const seenReviews = new Set();
1071
+ let reviewWarning = false;
1072
+ let timedOutReview = false;
1073
+ const retryMode = approvalMode[0].toUpperCase() + approvalMode.slice(1);
1074
+ const reviewNotice = (status, action) => {
1075
+ if (status === "warning") {
1076
+ emit({ ...base(threadId, turnId), type: "runtime.error",
1077
+ message: `Codex automatic review reported a timeout. Check what ran before retrying. Retry stays ${retryMode}. Select Ask for human approval in approval settings.`,
1078
+ });
1079
+ return;
1080
+ }
1081
+ const command = action?.type === "command" ? commandSummary({ command: action.command }) : undefined;
1082
+ const target = command ? `: "${command.slice(0, 30)}"` : " for the requested action";
1083
+ const outcome = status === "timedOut" ? "timed out" : "denied";
1084
+ emit({ ...base(threadId, turnId), type: "runtime.error",
1085
+ message: `Codex automatic review ${outcome}${target}. Action did not run. Retry stays ${retryMode}. Select Ask for human approval in approval settings.`,
1086
+ });
1087
+ };
963
1088
  const handleNotification = (msg) => {
964
1089
  const p = msg.params ?? {};
965
1090
  // An app-server also emits notifications for native helper threads.
@@ -997,10 +1122,32 @@ export const CodexDriver = {
997
1122
  }
998
1123
  const eventTurnId = msg.method === "turn/started" || msg.method === "turn/completed"
999
1124
  ? p.turn?.id : p.turnId;
1000
- if (eventTurnId !== codexTurnId)
1125
+ // guardianWarning is thread-scoped in Codex 0.147; this child
1126
+ // process belongs to one app turn. Never admit a mismatched turnId.
1127
+ if (eventTurnId !== codexTurnId && !(msg.method === "guardianWarning" && eventTurnId === undefined))
1001
1128
  return;
1002
1129
  }
1003
1130
  switch (msg.method) {
1131
+ case "guardianWarning":
1132
+ if (typeof p.message === "string" && /automatic approval review.*timed out/i.test(p.message))
1133
+ reviewWarning = true;
1134
+ break;
1135
+ case "item/autoApprovalReview/completed": {
1136
+ const status = p.review?.status;
1137
+ if (status !== "timedOut" && status !== "denied")
1138
+ break;
1139
+ // Completed reviews carry a reviewId. Without it distinct
1140
+ // failures cannot be separated from duplicate notifications.
1141
+ if (typeof p.reviewId !== "string" || !p.reviewId)
1142
+ break;
1143
+ if (!seenReviews.has(p.reviewId)) {
1144
+ seenReviews.add(p.reviewId);
1145
+ if (status === "timedOut")
1146
+ timedOutReview = true;
1147
+ reviewNotice(status, p.action);
1148
+ }
1149
+ break;
1150
+ }
1004
1151
  // token-level chat text; the item/completed frame follows with the
1005
1152
  // whole message, so its delta is only a fallback when none streamed
1006
1153
  case "item/agentMessage/delta": {
@@ -1135,8 +1282,10 @@ export const CodexDriver = {
1135
1282
  break;
1136
1283
  }
1137
1284
  case "turn/completed": {
1285
+ if (reviewWarning && !timedOutReview)
1286
+ reviewNotice("warning");
1138
1287
  const t = p.turn ?? {};
1139
- const message = typeof t.error?.message === "string" ? t.error.message.slice(0, 400) : "";
1288
+ const message = typeof t.error?.message === "string" ? codexUserError(t.error.message, plan) : "";
1140
1289
  if (t.status !== "completed" && message && message !== state.lastError) {
1141
1290
  state.lastError = message;
1142
1291
  emit({ ...base(threadId, turnId), type: "runtime.error", message,
@@ -1153,7 +1302,7 @@ export const CodexDriver = {
1153
1302
  {
1154
1303
  const message = p.message ?? p.error?.message;
1155
1304
  if (message) {
1156
- state.lastError = String(message).slice(0, 400);
1305
+ state.lastError = codexUserError(String(message), plan);
1157
1306
  emit({ ...base(threadId, turnId), type: "runtime.error", message: state.lastError });
1158
1307
  }
1159
1308
  }
@@ -1310,7 +1459,7 @@ export const CodexDriver = {
1310
1459
  // nothing streamed yet, and never for auth/shape errors or interrupts
1311
1460
  try {
1312
1461
  await request("initialize", {
1313
- clientInfo: { name: "openmausbot", version: "1" },
1462
+ clientInfo: { name: "openmausbot", title: "OpenMausBot", version: serverVersion() },
1314
1463
  // Named permission profiles are an experimental app-server field in
1315
1464
  // Codex 0.151. Negotiate them explicitly; older servers ignore this
1316
1465
  // capability and remain on the legacy Custom fallback below.
@@ -1338,6 +1487,11 @@ export const CodexDriver = {
1338
1487
  ? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
1339
1488
  : "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
1340
1489
  }
1490
+ // Proven before the turn starts: a Codex that did not take the
1491
+ // overrides runs nothing for a guest.
1492
+ if (turn.guestConfined && !codexShellDisabled(effectiveConfig)) {
1493
+ throw new Error(`This Codex could not turn its shell off, so it can't run this turn. Update Codex, or switch this bot to Claude.${turn.confinedWhy ? ` ${turn.confinedWhy}` : ""}`);
1494
+ }
1341
1495
  // Only the stable half of the prompt belongs in the developer slot:
1342
1496
  // it is the part that must survive compaction unchanged, and any
1343
1497
  // change to it invalidates the provider's cached prefix. The volatile
@@ -1368,6 +1522,11 @@ export const CodexDriver = {
1368
1522
  // Each turn launches a new app-server. Reassert current bot instructions
1369
1523
  // on start AND resume so Codex owns their lifetime through compaction.
1370
1524
  // Removed bot rules are cleared without dropping native configured rules.
1525
+ const selection = config.managed
1526
+ ? { model: turn.model, modelProvider: "openmaus_company" }
1527
+ : config.authMode === "chatgpt-plan"
1528
+ ? { model: turn.model, modelProvider: "openai_chatgpt_plan" }
1529
+ : decodeCodexSelection(turn.model);
1371
1530
  const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
1372
1531
  let startedModel = null;
1373
1532
  let resumedNativeThread = false;
@@ -1377,6 +1536,8 @@ export const CodexDriver = {
1377
1536
  const resumeThread = () => request("thread/resume", {
1378
1537
  threadId: cursor,
1379
1538
  developerInstructions,
1539
+ model: selection.model,
1540
+ ...(selection.modelProvider ? { modelProvider: selection.modelProvider } : {}),
1380
1541
  ...approvalParams.thread,
1381
1542
  });
1382
1543
  try {
@@ -1419,13 +1580,13 @@ export const CodexDriver = {
1419
1580
  }
1420
1581
  }
1421
1582
  if (!codexThreadId) {
1422
- const selection = config.managed ? { model: turn.model, modelProvider: "openmaus_company" } : decodeCodexSelection(turn.model);
1423
1583
  const startThread = () => request("thread/start", {
1424
1584
  developerInstructions,
1425
1585
  cwd: turn.cwd ?? homedir(),
1426
1586
  model: selection.model,
1427
1587
  ...(selection.modelProvider ? { modelProvider: selection.modelProvider } : {}),
1428
1588
  ...approvalParams.thread,
1589
+ ...(turn.guestConfined ? { environments: [] } : {}),
1429
1590
  ephemeral: false,
1430
1591
  });
1431
1592
  let started;
@@ -1464,6 +1625,10 @@ export const CodexDriver = {
1464
1625
  threadId: codexThreadId,
1465
1626
  input: turnInput,
1466
1627
  ...approvalParams.turn,
1628
+ // No environment: no exec_command, apply_patch or view_image, and
1629
+ // any call to them is refused (probed against codex-cli 0.159.0).
1630
+ // Without the experimental API the field is rejected, not ignored.
1631
+ ...(turn.guestConfined ? { environments: [] } : {}),
1467
1632
  // Spread, not `effort: turn.effort ?? null`. Probed against
1468
1633
  // codex-cli 0.146.0: null is indistinguishable from an absent key
1469
1634
  // — both leave the thread's current effort alone, emitting no
@@ -1492,7 +1657,7 @@ export const CodexDriver = {
1492
1657
  }
1493
1658
  catch (e) {
1494
1659
  const failure = e instanceof Error ? e : { text: String(e) };
1495
- const message = e instanceof Error ? e.message : String(e);
1660
+ const message = codexUserError(e instanceof Error ? e.message : String(e), plan);
1496
1661
  const needsAuth = /(?:\b401\b|unauthorized|missing bearer|authentication required)/i.test(message);
1497
1662
  const verdict = classifyError(failure);
1498
1663
  // Three guards hold here: main's abandoned attempt never retries,
@@ -1541,12 +1706,17 @@ export const CodexDriver = {
1541
1706
  return { turnId };
1542
1707
  };
1543
1708
  const snapshot = async () => {
1709
+ if (planUnavailable)
1710
+ return { state: "unavailable", authenticated: false, chatgptPlan: true, reason: planUnavailable, authenticationUnavailableReason: planUnavailable };
1544
1711
  const env = childEnv();
1545
1712
  const version = await new Promise((resolve) => {
1546
1713
  execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim()));
1547
1714
  });
1548
1715
  if (!version)
1549
- return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
1716
+ return { state: "unavailable", reason: `\`${config.cli}\` CLI not found`, ...(plan ? { chatgptPlan: true } : {}) };
1717
+ if (planAuth)
1718
+ return { state: "available", version, chatgptPlan: true, billing: "subscription", ...await planAuth.snapshot(), update: await codexReleaseUpdate(version, config.cli),
1719
+ ...(planWarning ? { warning: { title: "Check ChatGPT connection", message: planWarning } } : {}) };
1550
1720
  if (config.managed)
1551
1721
  return { state: "available", version, authenticated: Boolean(input.environment.OPENMAUSBOT_COMPANY_API_KEY && input.environment.CODEX_HOME), billing: "metered" };
1552
1722
  const authenticated = await new Promise((resolve) => {
@@ -1574,15 +1744,52 @@ export const CodexDriver = {
1574
1744
  return models;
1575
1745
  },
1576
1746
  refreshModels,
1577
- startAuthentication: () => authentication.start(),
1578
- getAuthentication: (flowId) => authentication.get(flowId),
1579
- cancelAuthentication: () => authentication.cancel(),
1580
- signOut: () => authentication.signOut(),
1747
+ ...(plan ? { authenticationMethod: "browser-pkce" } : {}),
1748
+ startAuthentication: async () => {
1749
+ if (planUnavailable)
1750
+ throw new Error(planUnavailable);
1751
+ if (planSigningOut || disposed)
1752
+ throw new Error("This account is being disconnected. Wait before signing in again.");
1753
+ return (planAuth ?? authentication).start();
1754
+ },
1755
+ getAuthentication: async (flowId) => {
1756
+ const result = await (planAuth ?? authentication).get(flowId);
1757
+ if (planAuth && result.phase === "succeeded")
1758
+ planWarning = undefined;
1759
+ if (planAuth && result.phase === "succeeded" && !models.options.length)
1760
+ await refreshModels();
1761
+ return result;
1762
+ },
1763
+ cancelAuthentication: () => (planAuth ?? authentication).cancel(),
1764
+ signOut: async () => {
1765
+ if (!planAuth)
1766
+ return authentication.signOut();
1767
+ planSigningOut = true;
1768
+ planGeneration++;
1769
+ try {
1770
+ const stopped = await Promise.all([...active.values()].map(({ stop }) => stop()));
1771
+ if (stopped.some(value => !value))
1772
+ throw new Error("A ChatGPT task could not stop safely. Stop the task before signing out.");
1773
+ models = { default: "", options: [] };
1774
+ await planAuth.signOut();
1775
+ planWarning = undefined;
1776
+ }
1777
+ catch (error) {
1778
+ if (error.code !== "chatgpt_revocation_unconfirmed")
1779
+ throw error;
1780
+ planWarning = error.message;
1781
+ }
1782
+ finally {
1783
+ planSigningOut = false;
1784
+ }
1785
+ },
1581
1786
  snapshot,
1582
1787
  adapter: {
1583
1788
  provider: DRIVER_KIND,
1584
1789
  capabilities: {
1585
1790
  sessionModelSwitch: "unsupported",
1791
+ // A guest's turn runs with no environment and the shell off (guestConfined).
1792
+ guestTurns: "confined",
1586
1793
  queueing: true,
1587
1794
  computerMcp: true,
1588
1795
  localComputerMcp: true,
@@ -1622,7 +1829,10 @@ export const CodexDriver = {
1622
1829
  },
1623
1830
  },
1624
1831
  dispose: async () => {
1832
+ disposed = true;
1833
+ planGeneration++;
1625
1834
  await authentication.dispose();
1835
+ await planAuth?.dispose();
1626
1836
  await Promise.all([...active.values()].map(({ stop }) => stop()));
1627
1837
  listeners.clear();
1628
1838
  },
@@ -38,8 +38,8 @@ export const GrokDriver = {
38
38
  models: () => MODELS,
39
39
  requestBody: (model, messages, stream) => ({ model, messages, stream }),
40
40
  httpErrorLabel: "xAI",
41
- missingKeyError: `no xAI key — set ${config.apiKeyEnv} or config.json xai.key`,
42
- unavailableReason: `no xAI API key — add {"xai":{"key":"xai-…"}} to ~/.openmausbot/config.json or set ${config.apiKeyEnv}`,
41
+ missingKeyError: `Save an xAI API key in Settings → API keys, or set ${config.apiKeyEnv}.`,
42
+ unavailableReason: "No xAI API key — open Settings → API keys.",
43
43
  timeoutMs: 120_000,
44
44
  retryScale: Number(process.env.FAKE_GROK_RETRY_SCALE ?? "1"),
45
45
  generateModel: () => "grok-3-mini",
@@ -45,7 +45,8 @@ export const MistralDriver = {
45
45
  models: DEFAULT_MODELS,
46
46
  install: {
47
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.",
48
+ settings: "connections",
49
+ signInCommand: "Save a Mistral API key in Settings → API keys, or set MISTRAL_API_KEY on the server.",
49
50
  },
50
51
  decodeConfig,
51
52
  defaultConfig: () => decodeConfig({}),
@@ -102,8 +103,8 @@ export const MistralDriver = {
102
103
  tools: config.tools, models: () => catalog, refreshModels, contentText,
103
104
  requestBody: (model, messages, stream) => ({ model, messages, stream }),
104
105
  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.",
106
+ missingKeyError: "Save a Mistral API key in Settings → API keys, or set MISTRAL_API_KEY.",
107
+ unavailableReason: "No Mistral API key — open Settings → API keys.",
107
108
  timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
108
109
  nativeLog: {
109
110
  source: "mistral.chat.completions",
@@ -32,6 +32,7 @@ export class ChatToolCalls {
32
32
  return;
33
33
  if (!Array.isArray(value))
34
34
  throw new ChatProtocolError("provider returned invalid tool_calls");
35
+ const omitted = (field) => field === undefined || (streaming && field === null);
35
36
  for (const [position, raw] of value.entries()) {
36
37
  const delta = object(raw);
37
38
  const index = streaming ? delta?.index : position;
@@ -39,9 +40,9 @@ export class ChatToolCalls {
39
40
  throw new ChatProtocolError("provider returned an invalid tool-call index");
40
41
  }
41
42
  const call = this.calls.get(Number(index)) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
42
- if (delta.type !== undefined && delta.type !== "function")
43
+ if (!omitted(delta.type) && delta.type !== "function")
43
44
  throw new ChatProtocolError("unsupported tool-call type");
44
- if (delta.id !== undefined) {
45
+ if (!omitted(delta.id)) {
45
46
  if (typeof delta.id !== "string" || !delta.id || delta.id.length > 256 || (call.id && call.id !== delta.id)) {
46
47
  throw new ChatProtocolError("provider changed or omitted a tool-call ID");
47
48
  }
@@ -52,7 +53,7 @@ export class ChatToolCalls {
52
53
  if (!fn)
53
54
  throw new ChatProtocolError("provider returned an invalid tool-call function");
54
55
  for (const field of ["name", "arguments"]) {
55
- if (fn[field] === undefined)
56
+ if (omitted(fn[field]))
56
57
  continue;
57
58
  if (typeof fn[field] !== "string")
58
59
  throw new ChatProtocolError("provider returned invalid tool-call arguments or name");
@@ -48,11 +48,32 @@ function rejectsToolsParameter(status, body) {
48
48
  /\b(?:tools|tool calling|function calling) (?:is|are) not supported\b/i.test(message) ||
49
49
  /^(?:unsupported|unknown|unrecognized) (?:parameter|field):?\s*['"]?tools['"]?[.!]?$/i.test(message.trim()));
50
50
  }
51
+ class UnsupportedReasoningReplayError extends Error {
52
+ }
53
+ /** DeepSeek-style providers need `reasoning_content` echoed on a tool-call
54
+ * assistant message; strict endpoints such as Groq reject the property. */
55
+ function rejectsReasoningReplay(status, body) {
56
+ if (status !== 400 && status !== 422)
57
+ return false;
58
+ let envelope;
59
+ try {
60
+ envelope = object(JSON.parse(body));
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ const message = object(envelope?.error)?.message ?? envelope?.error;
66
+ return typeof message === "string" && /\breasoning_content\b/.test(message) &&
67
+ /\b(?:unsupported|not supported|unknown|unrecognized|unexpected|not permitted|not allowed)\b/i.test(message);
68
+ }
51
69
  /** Shared runtime for the three providers that speak OpenAI chat completions. */
52
70
  export function createOpenAIChatRuntime(options) {
53
71
  const { input } = options;
54
72
  const listeners = new Set();
55
73
  const active = new Map();
74
+ /** Models whose endpoint rejected an echoed `reasoning_content`. In memory
75
+ * only: later rounds and turns omit the field instead of failing again. */
76
+ const reasoningReplayRejected = new Set();
56
77
  const emit = (event) => {
57
78
  for (const listener of Array.from(listeners))
58
79
  listener(event);
@@ -95,6 +116,9 @@ export function createOpenAIChatRuntime(options) {
95
116
  const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
96
117
  if (rejectsToolsParameter(response.status, body))
97
118
  throw new UnsupportedChatToolsError(message);
119
+ if (messages.some((entry) => entry.reasoning_content !== undefined) && rejectsReasoningReplay(response.status, body)) {
120
+ throw new UnsupportedReasoningReplayError(message);
121
+ }
98
122
  throw new Error(message);
99
123
  }
100
124
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
@@ -377,7 +401,7 @@ export function createOpenAIChatRuntime(options) {
377
401
  const parked = turnEntry.asides.splice(0);
378
402
  if (parked.length)
379
403
  messages.push({ role: "user", content: parked.join("\n\n") });
380
- native("out", options.nativeLog.outgoing(turn, messages, model));
404
+ native("out", options.nativeLog.outgoing(turn, messages, model, tools.definitions));
381
405
  let attempt = 0;
382
406
  let completion;
383
407
  for (;;) {
@@ -419,6 +443,16 @@ export function createOpenAIChatRuntime(options) {
419
443
  catch (value) {
420
444
  const error = asError(value);
421
445
  const verdict = classifyError(error);
446
+ // A schema rejection of the echoed reasoning executed nothing
447
+ // upstream, so the same transcript without that one property
448
+ // repeats no operation. The error needs the field in the
449
+ // request, so the stripped resend cannot loop.
450
+ if (error instanceof UnsupportedReasoningReplayError && !streamed && !abort.signal.aborted) {
451
+ reasoningReplayRejected.add(model);
452
+ for (const message of messages)
453
+ delete message.reasoning_content;
454
+ continue;
455
+ }
422
456
  // Only our optional question changed a previously plain request.
423
457
  // A structured parameter rejection has executed nothing; never
424
458
  // downgrade mounted tools, streamed output or a handled call.
@@ -474,7 +508,7 @@ export function createOpenAIChatRuntime(options) {
474
508
  if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
475
509
  throw new ChatProtocolError("tool-call limit reached");
476
510
  messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
477
- ...(completion.protocolReasoning ? { reasoning_content: completion.protocolReasoning } : {}),
511
+ ...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { reasoning_content: completion.protocolReasoning } : {}),
478
512
  ...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
479
513
  });
480
514
  const screenshotParts = [];
@@ -637,7 +671,11 @@ export function createOpenAIChatRuntime(options) {
637
671
  sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
638
672
  // The runtime owns the whole tool loop, so it can always take a
639
673
  // user message mid-turn: park it, deliver before the next completion.
640
- queueing: true },
674
+ queueing: true,
675
+ // No shell and no file tool on the host at all: only MCP tools, each
676
+ // call a card in Ask (the Boat's `exec` runs on the Boat). A guest's
677
+ // turn is as confined as it gets.
678
+ guestTurns: "confined" },
641
679
  sendTurn,
642
680
  interruptTurn: async (threadId, turnId) => {
643
681
  const turn = active.get(threadId);