openmausbot 0.1.90 → 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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.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 +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  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 +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -13,6 +13,7 @@
13
13
  import { createHash } from "node:crypto";
14
14
  import { DATA_DIR } from "./config.js";
15
15
  import { loadEnvironmentId } from "./environment.js";
16
+ import { boatCredential, boatProviderApi } from "./included-services.js";
16
17
  import { adoptResolvedBoat, beginBoatCreate, boatCreateRecoverySnapshot, discardBoatCreate, rememberCreatedBoat, resolveBoatCreate, retireDeletedBoatCreate, } from "./boat-create-idempotency.js";
17
18
  import { boatDeletionSnapshot, getBoatDeletion, hasPendingBoatDeletionForBot, markBoatDeletionAccepted, markBoatDeletionBlocked, prepareBoatDeletion, retireBoatDeletion, } from "./boat-delete-journal.js";
18
19
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -34,10 +35,8 @@ export function isolatedRemoteCommand(command) {
34
35
  shellQuote(command),
35
36
  ].join(" ");
36
37
  }
37
- // overridable so tests can point at a stub instead of the live provider.
38
38
  // Boat's provider surface keeps its historical Box-era names: env OMB_BOX_API,
39
39
  // base path /api/box/v1, REST paths /boxes/*, and the box_ token prefix.
40
- const BOAT_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
41
40
  const READY = new Set(["idle", "ready", "running"]);
42
41
  const SLEEPING = new Set(["archived", "archiving", "stopped", "stopping"]);
43
42
  const DEFAULT_BOAT_TTL_SECONDS = 8 * 60 * 60;
@@ -105,11 +104,15 @@ export function boatTurnLifecycleAction({ explicitCloud, canMount, state, }) {
105
104
  function snapshotBoatConfig(cfg) {
106
105
  return { box: cfg.box ? { token: cfg.box.token } : undefined };
107
106
  }
107
+ /** The base URL follows the credential in use (included-services.ts): an own
108
+ * token goes to Boat (OMB_BOX_API points it at a stub in tests), Cloud Pro's
109
+ * included token only to its relay. */
108
110
  function boatFetch(cfg, path, opts = {}) {
109
- return fetch(`${BOAT_API}${path}`, {
111
+ const account = boatAccount(cfg);
112
+ return fetch(`${account?.api ?? boatProviderApi()}${path}`, {
110
113
  ...opts,
111
114
  headers: {
112
- authorization: `Bearer ${cfg.box?.token}`,
115
+ authorization: `Bearer ${account?.token}`,
113
116
  "content-type": "application/json",
114
117
  ...opts.headers,
115
118
  },
@@ -280,7 +283,7 @@ async function requestRecordedBoatDeletion(cfg, identity, pollDelaysMs = BOAT_DE
280
283
  }
281
284
  if (!removed.ok) {
282
285
  markBoatDeletionBlocked(identity.boxId);
283
- throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body)), { status: removed.status });
286
+ throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body, usesIncludedBoat(cfg))), { status: removed.status });
284
287
  }
285
288
  const confirmation = await confirmAcceptedBoatDeletion(cfg, deletion, removed.body, pollDelaysMs);
286
289
  if (confirmation === "blocked")
@@ -346,6 +349,8 @@ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
346
349
  async function waitReady(cfg, boxId, budgetMs = 90_000) {
347
350
  assertBoatNotDeleting(boxId);
348
351
  const t0 = Date.now();
352
+ // Boat's words for the last failed resume, if the wait runs out on them.
353
+ let resumeFailure = null;
349
354
  while (Date.now() - t0 < budgetMs) {
350
355
  assertBoatNotDeleting(boxId);
351
356
  const { body } = await boatJson(cfg, `/boxes/${boxId}`);
@@ -354,11 +359,25 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
354
359
  return body.box;
355
360
  if (state === "error")
356
361
  return null;
357
- // an archiving boat can't resume until the snapshot lands — nudge after
358
- if (state === "archived")
359
- await boatJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
362
+ // an archiving boat can't resume until the snapshot lands — nudge after.
363
+ // A refusal (a plan limit, say) is final: report it now. A server error
364
+ // is retried on the next poll, as Boat asks; 409 is a state race with a
365
+ // wake already under way.
366
+ if (state === "archived") {
367
+ const resumed = await boatJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
368
+ if (resumed.ok)
369
+ resumeFailure = null;
370
+ else if (resumed.status !== 409) {
371
+ const message = boatErrorMessage(resumed.status, "waking the cloud computer", resumed.body, usesIncludedBoat(cfg));
372
+ if (resumed.status < 500)
373
+ throw new Error(message);
374
+ resumeFailure = message;
375
+ }
376
+ }
360
377
  await new Promise((r) => setTimeout(r, 2500));
361
378
  }
379
+ if (resumeFailure)
380
+ throw new Error(resumeFailure);
362
381
  return null;
363
382
  }
364
383
  // Resolving a bot's boat means LISTing every boat in the account, so it is
@@ -367,9 +386,9 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
367
386
  // whenever the direct read fails (deleted/renamed boat) and always carries
368
387
  // the live state so callers can still see "archived".
369
388
  const boatIdCache = new Map();
370
- function boatInventoryProblem(status, body) {
389
+ function boatInventoryProblem(status, body, included = false) {
371
390
  if (status === 401 || status === 403) {
372
- return "boat.dev rejected the Boat API key — update it in Settings → Connections";
391
+ return included ? INCLUDED_BOAT_UNAVAILABLE : "boat.dev rejected the Boat API key — update it in Settings → API keys";
373
392
  }
374
393
  if (status === 429)
375
394
  return "boat.dev is rate-limiting this account — wait a minute and refresh";
@@ -396,7 +415,11 @@ async function listBoatPages(cfg) {
396
415
  return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
397
416
  }
398
417
  if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
399
- return { ok: false, problem: boatInventoryProblem(listed.status, listed.body), credentialRejected: listed.status === 401 || listed.status === 403 };
418
+ return {
419
+ ok: false,
420
+ problem: boatInventoryProblem(listed.status, listed.body, usesIncludedBoat(cfg)),
421
+ credentialRejected: listed.status === 401 || listed.status === 403,
422
+ };
400
423
  }
401
424
  boats.push(...listed.body.boxes);
402
425
  const next = listed.body?.pageInfo?.nextCursor;
@@ -672,7 +695,7 @@ export async function inspectBoatIdentity(cfg, boxId) {
672
695
  return { available: true, identity: null, problem: null };
673
696
  }
674
697
  if (!inspected.ok) {
675
- return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body) };
698
+ return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body, usesIncludedBoat(cfg)) };
676
699
  }
677
700
  const candidate = inspected.body?.box;
678
701
  const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
@@ -685,7 +708,7 @@ export async function inspectBoatIdentity(cfg, boxId) {
685
708
  function inventoryFailure(inventory) {
686
709
  const error = new Error(inventory.configured
687
710
  ? (inventory.problem ?? "Cloud computer inventory is unavailable")
688
- : "Boat is not configured — add its API key in Settings → Connections");
711
+ : "Boat is not configured — add its API key in Settings → API keys");
689
712
  error.status = inventory.configured ? 503 : 409;
690
713
  return error;
691
714
  }
@@ -722,8 +745,9 @@ async function stopBoat(cfg, boxId) {
722
745
  // must never say a computer is sleeping when boat.dev rejected the action.
723
746
  await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
724
747
  const stopped = await boatJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
725
- if (!stopped.ok)
726
- throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body)), { status: stopped.status });
748
+ if (!stopped.ok) {
749
+ throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body, usesIncludedBoat(cfg))), { status: stopped.status });
750
+ }
727
751
  }
728
752
  function forgetBoatId(boxId) {
729
753
  for (const [botId, cachedId] of boatIdCache) {
@@ -854,15 +878,30 @@ export async function readyBoat(cfg, botId, budgetMs = 60_000) {
854
878
  return boat;
855
879
  return waitReady(cfg, boat.id, budgetMs);
856
880
  }
881
+ /** The Boat credential a request uses: the person's own token, else Cloud
882
+ * Pro's included one. Settings' own-key flows read cfg.box.token instead. */
883
+ export function boatAccount(cfg) {
884
+ return boatCredential(cfg.box?.token);
885
+ }
857
886
  export function boatConfigured(cfg) {
858
- return Boolean(cfg.box?.token);
887
+ return Boolean(boatAccount(cfg));
888
+ }
889
+ function usesIncludedBoat(cfg) {
890
+ return boatAccount(cfg)?.included === true;
891
+ }
892
+ /** What Settings shows: configured-or-not, and whether that is Cloud Pro's
893
+ * included account rather than a saved key. Never the token. */
894
+ export function describeBoatAccount(cfg) {
895
+ const account = boatAccount(cfg);
896
+ return { configured: Boolean(account), ...(account?.included ? { included: true } : {}) };
859
897
  }
860
898
  /** Ask the provider whether a token is real, before we let someone save
861
899
  * it. Without this the paste "succeeds", and the first sign of trouble is
862
- * a 401 in a different panel minutes later, with nothing to act on. */
900
+ * a 401 in a different panel minutes later, with nothing to act on. Only
901
+ * ever an own token, so only ever Boat itself. */
863
902
  export async function verifyToken(token) {
864
903
  try {
865
- const res = await fetch(`${BOAT_API}/boxes`, {
904
+ const res = await fetch(`${boatProviderApi()}/boxes`, {
866
905
  headers: { authorization: `Bearer ${token}` },
867
906
  signal: AbortSignal.timeout(20_000),
868
907
  });
@@ -884,11 +923,15 @@ export async function verifyToken(token) {
884
923
  return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
885
924
  }
886
925
  }
926
+ /** A rejected Cloud Pro relay token: nothing the person pasted, so nothing
927
+ * for them to fix in Settings. */
928
+ const INCLUDED_BOAT_UNAVAILABLE = "Cloud Pro's included cloud computers aren't available right now. Try again later.";
887
929
  /** Turn a provider refusal into something a person can act on. The
888
930
  * provider's own message is better than anything we can invent — it knows
889
931
  * the plan, the limit and the link — so prefer it and only fall back to
890
- * our own wording when it says nothing useful. */
891
- export function boatErrorMessage(status, what, body) {
932
+ * our own wording when it says nothing useful. `included`: the request used
933
+ * Cloud Pro's included token, not the person's own. */
934
+ export function boatErrorMessage(status, what, body, included = false) {
892
935
  const theirs = typeof body?.message === "string" ? body.message.trim() : "";
893
936
  const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
894
937
  if (status === 402) {
@@ -896,6 +939,8 @@ export function boatErrorMessage(status, what, body) {
896
939
  return [theirs || "boat.dev needs a paid Boat plan before it will create a computer.", link].filter(Boolean).join(" ");
897
940
  }
898
941
  if (status === 401 || status === 403) {
942
+ if (included)
943
+ return INCLUDED_BOAT_UNAVAILABLE;
899
944
  return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
900
945
  }
901
946
  if (status === 429) {
@@ -1089,7 +1134,7 @@ export async function provisionBoat(cfg, botId, _botName) {
1089
1134
  // retry when boat.dev reports their shorter TTL ceiling.
1090
1135
  const createRes = await createBoat(cfg, botId, credentialEnv);
1091
1136
  if (!createRes.ok || !createRes.body?.box?.id) {
1092
- throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body));
1137
+ throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg)));
1093
1138
  }
1094
1139
  boat = createRes.body.box;
1095
1140
  createRequest = createRes.request;
@@ -1099,7 +1144,7 @@ export async function provisionBoat(cfg, botId, _botName) {
1099
1144
  body: JSON.stringify({ name: vmName }),
1100
1145
  });
1101
1146
  if (!rename.ok)
1102
- throw new Error(boatErrorMessage(rename.status, "box naming", rename.body));
1147
+ throw new Error(boatErrorMessage(rename.status, "box naming", rename.body, usesIncludedBoat(cfg)));
1103
1148
  if (createRequest)
1104
1149
  createRequest = resolveBoatCreate(createRequest);
1105
1150
  }
@@ -144,14 +144,17 @@ export function grantsSummary(connectorTools) {
144
144
  })
145
145
  .sort((a, b) => (a.slug < b.slug ? -1 : 1));
146
146
  }
147
- function computerReach(computer) {
147
+ function computerReach(computer, cloudHome = false) {
148
+ // A Cloud home never offers either; say so rather than list a preference
149
+ // that every task there refuses.
150
+ const unavailable = cloudHome ? ", which isn't available on OMB Cloud" : "";
148
151
  switch (computer) {
149
152
  case "cloud":
150
153
  return "Computer preference: cloud computer.";
151
154
  case "vm":
152
- return "Computer preference: Local VM.";
155
+ return `Computer preference: Local VM${unavailable}.`;
153
156
  case "local":
154
- return "Computer preference: this computer.";
157
+ return `Computer preference: this computer${unavailable}.`;
155
158
  case "browser":
156
159
  return "Computer preference: browser only.";
157
160
  case "off":
@@ -162,7 +165,7 @@ function computerReach(computer) {
162
165
  }
163
166
  function reachesLines(facts) {
164
167
  const lines = [];
165
- const computer = computerReach(facts.bot.computer);
168
+ const computer = computerReach(facts.bot.computer, facts.cloudHome);
166
169
  if (computer)
167
170
  lines.push(computer);
168
171
  lines.push(facts.bot.cwd ? `Works in ${facts.bot.cwd}.` : "Works in its private workspace.");
@@ -442,8 +442,17 @@ export function agentBrowserIntegration(input) {
442
442
  ...(sourceEnv.USERPROFILE ? { USERPROFILE: sourceEnv.USERPROFILE } : {}),
443
443
  })),
444
444
  };
445
- if (input.headless !== false)
445
+ // A Linux host with no display cannot run headed Chrome, and OMB's managed
446
+ // browser is headless automation (docs/browser-packaging.md). Pin headless
447
+ // there even for a headed-capable caller. The engine's own flag is
448
+ // AGENT_BROWSER_HEADED; an explicit "0" pins the default so a daemon that
449
+ // kept a headed launch config still comes back headless instead of failing
450
+ // every launch until it is killed by hand (#1383).
451
+ const displayless = (input.platform ?? process.platform) === "linux" && !sourceEnv.DISPLAY && !sourceEnv.WAYLAND_DISPLAY;
452
+ if (input.headless !== false || displayless) {
446
453
  env.AGENT_BROWSER_HEADLESS = "1";
454
+ env.AGENT_BROWSER_HEADED = "0";
455
+ }
447
456
  if (input.attachCdpUrl)
448
457
  env.AGENT_BROWSER_CDP = input.attachCdpUrl;
449
458
  // MCP clients may filter the parent environment. Carry the configured
@@ -39,6 +39,19 @@ function displayUrl(value) {
39
39
  return "";
40
40
  }
41
41
  }
42
+ /** Chrome's own launch-failure wording: the engine is installed and its
43
+ * daemon answers, but no browser can start (no display, an early exit).
44
+ * Distinct from a missing engine — the fix is a fresh session, not an
45
+ * install, so the generic "check the engine is installed" guidance would
46
+ * send the operator the wrong way (#1383). */
47
+ function browserLaunchFailure(error) {
48
+ const detail = [
49
+ error?.stderr,
50
+ error?.stdout,
51
+ error instanceof Error ? error.message : String(error),
52
+ ].filter((part) => typeof part === "string").join("\n");
53
+ return /missing x server|\$DISPLAY|devtoolsactiveport|chrome exited|could not launch|failed to launch|browser launch failed/iu.test(detail);
54
+ }
42
55
  /** Project only the viewer protocol. Never relay engine commands, results or console output. */
43
56
  export function normalizeBrowserLiveMessage(value) {
44
57
  const message = object(value);
@@ -150,6 +163,7 @@ export function parseBrowserLiveAction(value) {
150
163
  export class BrowserLive {
151
164
  runtime;
152
165
  viewers = new Map();
166
+ sessionResets = new Map();
153
167
  constructor({ runtime }) { this.runtime = runtime; }
154
168
  current(viewer) {
155
169
  try {
@@ -298,12 +312,37 @@ export class BrowserLive {
298
312
  throw new Error("stale viewer");
299
313
  const result = object(JSON.parse(stdout));
300
314
  const data = object(result?.data);
301
- if (result?.success !== true || !data)
302
- throw new Error("browser command failed");
315
+ if (result?.success !== true || !data) {
316
+ // Keep the engine's own error internal (logged, never sent) so a
317
+ // launch failure is recognized for what it is below.
318
+ const detail = [result?.error, result?.message].filter((part) => typeof part === "string").join(" ");
319
+ throw new Error(detail ? `browser command failed: ${detail}` : "browser command failed");
320
+ }
303
321
  return data;
304
322
  }
305
323
  catch (error) {
306
324
  console.warn("browser-live:", error);
325
+ if (browserLaunchFailure(error)) {
326
+ // The daemon is alive but holding a launch configuration Chrome
327
+ // cannot satisfy on this host (a headed launch with no display). It
328
+ // fails every command the same way until the daemon is killed —
329
+ // discard the session so the next connect starts a fresh one. One
330
+ // tracked reset per session: a reconnect or repeated command joins
331
+ // the in-flight teardown instead of spawning another (#1383).
332
+ if (!this.sessionResets.has(viewer.session)) {
333
+ const reset = closeBrowserSession(viewer.spec.command, env).then((closed) => {
334
+ if (closed)
335
+ console.warn(`browser-live: reset ${viewer.session} after a failed browser launch; reconnecting starts a fresh session`);
336
+ else
337
+ console.warn(`browser-live: reset of ${viewer.session} after a failed browser launch did not finish; the next command retries it`);
338
+ return closed;
339
+ }, () => false);
340
+ this.sessionResets.set(viewer.session, reset);
341
+ void reset.finally(() => { if (this.sessionResets.get(viewer.session) === reset)
342
+ this.sessionResets.delete(viewer.session); });
343
+ }
344
+ throw new BrowserLiveError("The browser could not start on this server. Reconnect to retry with a fresh browser session.", 503);
345
+ }
307
346
  throw new BrowserLiveError("The browser could not complete this action. Check that the browser engine is installed, then reconnect.", 503);
308
347
  }
309
348
  }
@@ -380,6 +419,11 @@ export class BrowserLive {
380
419
  options.res.once("close", () => this.close(viewer));
381
420
  options.res.once("error", () => this.close(viewer));
382
421
  try {
422
+ // A failed launch already started this session's daemon teardown; wait
423
+ // for it so the status probe meets a fresh daemon, not the stuck one.
424
+ const resetting = this.sessionResets.get(viewer.session);
425
+ if (resetting)
426
+ await resetting;
383
427
  let status = await this.command(viewer, ["stream", "status"]);
384
428
  if (status.enabled !== true) {
385
429
  try {
@@ -1,6 +1,16 @@
1
1
  import { killCliTree, spawnCli } from "./procs.js";
2
2
  import { DEFAULT_BROWSER_RESULT_BUDGET, shapeBrowserToolResult, slimBrowserToolList, stripHarnessOwnedArguments } from "./browser-tool-shape.js";
3
3
  export const BROWSER_CONTROL_REFUSAL = "Browser tools are paused while a person controls this browser. Wait for them to hand control back; do not try another browser or execution tool.";
4
+ /** Harness-owned recovery tool, advertised beside the engine's own tools. A
5
+ * phone or chat-only user has no Browser panel Restart button; without this
6
+ * an interrupted action refuses the bot's browser until the app restarts. */
7
+ export const BROWSER_RESTART_TOOL = "restart_browser";
8
+ const BROWSER_RESTART_TOOL_SPEC = {
9
+ name: BROWSER_RESTART_TOOL,
10
+ description: "Close and restart your browser. Use this only when a browser tool says a browser action was interrupted and the browser must be restarted. Open pages are closed; afterwards open the page you need again and check whether the interrupted action already happened before repeating it.",
11
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
12
+ };
13
+ const BROWSER_INTERRUPTED = `A browser action was interrupted. Restart this browser with the ${BROWSER_RESTART_TOOL} tool before continuing.`;
4
14
  const MAX_REQUEST_BYTES = 1_048_576;
5
15
  const MAX_RESPONSE_BYTES = 16_777_216;
6
16
  /** Startup, not per-request work: a cold engine spawn can exceed a tight
@@ -200,10 +210,15 @@ class BrowserClient {
200
210
  export class BrowserRuntime {
201
211
  gates = new Map();
202
212
  clients = new Map();
213
+ /** Last advertised tools per session, so a turn that starts while the
214
+ * browser is uncertain still sees the tools it can use after recovering. */
215
+ toolLists = new Map();
203
216
  options;
204
- constructor(options = {}) {
217
+ closeBrowser;
218
+ constructor({ closeBrowser, ...options } = {}) {
205
219
  const budget = Number(process.env.OMB_BROWSER_RESULT_BUDGET);
206
220
  this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BROWSER_RESULT_BUDGET, ...options };
221
+ this.closeBrowser = closeBrowser ?? (async () => false);
207
222
  }
208
223
  gate(session) {
209
224
  let gate = this.gates.get(session);
@@ -227,7 +242,7 @@ export class BrowserRuntime {
227
242
  if (gate.owner !== null)
228
243
  throw new Error(BROWSER_CONTROL_REFUSAL);
229
244
  if (gate.uncertain)
230
- throw new Error("A browser action was interrupted. Restart this browser before continuing.");
245
+ throw new Error(BROWSER_INTERRUPTED);
231
246
  if (gate.closing)
232
247
  throw new Error("The browser is closing. Try again shortly.");
233
248
  gate.agents++;
@@ -250,10 +265,21 @@ export class BrowserRuntime {
250
265
  // must refuse the closing window itself or its client outlives restart().
251
266
  if (method !== "tools/call" && this.gate(session).closing)
252
267
  throw new Error("The browser is closing. Try again shortly.");
268
+ if (method === "tools/call" && params && typeof params === "object" && params.name === BROWSER_RESTART_TOOL) {
269
+ beforeDispatch?.();
270
+ await this.agentRestart(session, () => this.closeBrowser(session, spec));
271
+ beforeDispatch?.(); // A turn revoked while the browser closed receives no result.
272
+ return { content: [{ type: "text", text: "Browser restarted. Open pages were closed; open the page you need again and check whether the interrupted action already happened before repeating it." }] };
273
+ }
253
274
  // Uncertainty survives client replacement and never self-resolves: refuse
254
- // every new browser request until an explicit restart clears it.
255
- if (this.gate(session).uncertain)
256
- throw new Error("A browser action was interrupted. Restart this browser before continuing.");
275
+ // every new browser request until an explicit restart clears it. Listing
276
+ // never reaches the engine; it answers from the last list so the restart
277
+ // tool is reachable and the engine's tools are there once it succeeds.
278
+ if (this.gate(session).uncertain) {
279
+ if (method === "tools/list")
280
+ return withRestartTool(this.toolLists.get(session) ?? { tools: [] });
281
+ throw new Error(BROWSER_INTERRUPTED);
282
+ }
257
283
  const invoke = async () => {
258
284
  const key = JSON.stringify([spec.command, spec.args, Object.entries(spec.env).sort(([a], [b]) => a.localeCompare(b))]);
259
285
  let entry = this.clients.get(session);
@@ -281,8 +307,11 @@ export class BrowserRuntime {
281
307
  const request = method === "tools/call" ? stripHarnessOwnedArguments(params) : params;
282
308
  let result = await entry.client.rpc(method, request);
283
309
  beforeDispatch?.(); // A turn revoked while the tool ran receives no result.
284
- if (method === "tools/list")
285
- return slimBrowserToolList(result);
310
+ if (method === "tools/list") {
311
+ const tools = withRestartTool(slimBrowserToolList(result));
312
+ this.toolLists.set(session, tools);
313
+ return tools;
314
+ }
286
315
  const toolName = request && typeof request === "object" && typeof request.name === "string" ? request.name : undefined;
287
316
  if (toolName === "agent_browser_open" && result && typeof result === "object" &&
288
317
  result.isError !== true) {
@@ -448,6 +477,35 @@ export class BrowserRuntime {
448
477
  this.changed(gate);
449
478
  }
450
479
  }
480
+ /** The agent's own recovery, for a turn with no Browser panel to press
481
+ * Restart in. The same barrier as restart() — the native browser must
482
+ * close — but it never takes a person's hold, and a failed close leaves no
483
+ * owner behind that would lock a person out of the panel. */
484
+ async agentRestart(session, closeBrowser) {
485
+ const gate = this.gate(session);
486
+ if (gate.owner !== null)
487
+ throw new Error(BROWSER_CONTROL_REFUSAL);
488
+ if (gate.closing || gate.releasing || gate.agents || gate.humans)
489
+ throw new Error("The browser is busy. Wait for current work to finish before restarting.");
490
+ gate.ready = false;
491
+ gate.closing = true;
492
+ this.changed(gate);
493
+ try {
494
+ if (!await closeBrowser())
495
+ throw new Error("The browser could not be closed. Ask the person to press Restart in the Browser panel of OpenMausBot on their computer, or to restart OpenMausBot.");
496
+ await this.clients.get(session)?.client.stop();
497
+ await this.clients.get(session)?.client.stop(); // see restart()
498
+ gate.uncertain = false;
499
+ }
500
+ catch (error) {
501
+ gate.uncertain = true;
502
+ throw error;
503
+ }
504
+ finally {
505
+ gate.closing = false;
506
+ this.changed(gate);
507
+ }
508
+ }
451
509
  /** Call after the underlying browser is closed when recovering an uncertain
452
510
  * action. This closes the MCP transport, not saved logins or profile files. */
453
511
  async close(session) {
@@ -460,10 +518,18 @@ export class BrowserRuntime {
460
518
  await this.clients.get(session)?.client.stop();
461
519
  gate.closing = false;
462
520
  this.changed(gate);
463
- if (!gate.owner && !gate.agents && !gate.humans)
521
+ if (!gate.owner && !gate.agents && !gate.humans) {
464
522
  this.gates.delete(session);
523
+ this.toolLists.delete(session);
524
+ }
465
525
  }
466
526
  async closeAll() {
467
527
  await Promise.all([...new Set([...this.clients.keys(), ...this.gates.keys()])].map((session) => this.close(session)));
468
528
  }
469
529
  }
530
+ function withRestartTool(result) {
531
+ if (!result || typeof result !== "object" || !Array.isArray(result.tools))
532
+ return result;
533
+ const tools = (result.tools).filter((tool) => tool?.name !== BROWSER_RESTART_TOOL);
534
+ return { ...result, tools: [...tools, BROWSER_RESTART_TOOL_SPEC] };
535
+ }
@@ -12,11 +12,13 @@
12
12
  import { trimResultText } from "./mcp-trim.js";
13
13
  /** Launch, session and network settings OpenMausBot owns through the
14
14
  * environment (see browser-engine.ts). A model has no business setting them
15
- * per call — `session` would reach another bot's browser, `extraArgs` and
16
- * `caCert` change the launch — and each one cost more schema than the tool's
17
- * own description. */
15
+ * per call — `session` would reach another bot's browser, `extraArgs`,
16
+ * `caCert` and `headed` change the launch — and each one cost more schema
17
+ * than the tool's own description. `headed` in particular overrides
18
+ * AGENT_BROWSER_HEADED and the managed config, so a single call can pin a
19
+ * daemon to a launch mode this host cannot satisfy (#1383). */
18
20
  export const HARNESS_OWNED_BROWSER_PARAMS = new Set([
19
- "allowedDomains", "caCert", "clearCaCert", "extraArgs", "idleTimeout", "namespace",
21
+ "allowedDomains", "caCert", "clearCaCert", "extraArgs", "headed", "idleTimeout", "namespace",
20
22
  "restore", "restoreCheckFn", "restoreCheckText", "restoreCheckUrl", "restoreSave", "session", "timeoutMs",
21
23
  ]);
22
24
  /** Characters of one browser result that may enter the model's context.
@@ -29,7 +29,7 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
29
29
  });
30
30
  const delegation = canDelegate
31
31
  ? boundedCoordination
32
- ? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Outside a room, everything you send a teammate continues your one standing conversation with them, using their own model and permissions, so they still have the context of your earlier assignments. Busy teammates queue. Give self-contained briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
32
+ ? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Outside a room, each distinct assignment starts a fresh thread for that teammate, using their own model and permissions. Include the context they need in each brief. Busy teammates queue. Give self-contained briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
33
33
  : [
34
34
  "Use list_bots to confirm the live roster and IDs. When assigning work to a teammate, use delegate_bot: it returns immediately, keeps you available to the user, and delivers the teammate's outcome back into this conversation automatically — success or failure. When the result arrives you are woken with it: report it to the user and act. If the teammate fails or stalls, tell the user plainly and decide the next step yourself.",
35
35
  "After delegate_bot accepts the task, acknowledge the handoff and continue with any independent work or end your turn. Do not call wait_delegation or repeatedly poll check_delegation in the same turn.",