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
@@ -1,4 +1,4 @@
1
- // Box (box.ascii.dev) provider — the bot's cloud computer. Ported from
1
+ // Box (boat.dev) provider — the bot's cloud computer. Ported from
2
2
  // agentcal-api src/providers/box.js, reshaped per-bot instead of
3
3
  // per-customer: every bot gets one persistent box (deterministic name),
4
4
  // stop pauses billing while the disk survives, and Join always mints a
@@ -134,7 +134,7 @@ function boxDeletionOperation(body, boxId, expectedOperationId) {
134
134
  return { id, kind: "box", targetId: boxId, status: status };
135
135
  }
136
136
  function deletionBlockedError(boxId) {
137
- return Object.assign(new Error(`ascii.dev accepted deletion of ${boxId}, but the deletion operation is blocked — check ascii.dev and retry`), { status: 409 });
137
+ return Object.assign(new Error(`boat.dev accepted deletion of ${boxId}, but the deletion operation is blocked — check boat.dev and retry`), { status: 409 });
138
138
  }
139
139
  function boxDeleteProvedAbsent(result) {
140
140
  return result.status === 404 || result.status === 410;
@@ -194,7 +194,7 @@ async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
194
194
  return "confirmed";
195
195
  }
196
196
  if (inspected.identity.name !== record.name) {
197
- throw Object.assign(new Error("A cloud computer being deleted no longer has its remembered name — repair it in ascii.dev before continuing"), { status: 503 });
197
+ throw Object.assign(new Error("A cloud computer being deleted no longer has its remembered name — repair it in boat.dev before continuing"), { status: 503 });
198
198
  }
199
199
  return record.phase === "blocked" ? "blocked" : "pending";
200
200
  }
@@ -251,7 +251,7 @@ async function confirmAcceptedBoxDeletion(cfg, record, acceptedBody, pollDelaysM
251
251
  finishRecordedBoxDeletion(record.boxId);
252
252
  return "confirmed";
253
253
  }
254
- throw Object.assign(new Error(`ascii.dev returned an invalid deletion receipt for ${record.boxId}; its deletion fence was kept`), { status: 503 });
254
+ throw Object.assign(new Error(`boat.dev returned an invalid deletion receipt for ${record.boxId}; its deletion fence was kept`), { status: 503 });
255
255
  }
256
256
  const next = operation.status === "blocked"
257
257
  ? markBoxDeletionBlocked(record.boxId, operation)
@@ -270,7 +270,7 @@ async function requestRecordedBoxDeletion(cfg, identity, pollDelaysMs = BOX_DELE
270
270
  });
271
271
  }
272
272
  catch (error) {
273
- throw Object.assign(new Error("Could not confirm whether ascii.dev accepted the delete. The computer was kept fenced; retry Delete to reconcile it"), { status: 503, cause: error });
273
+ throw Object.assign(new Error("Could not confirm whether boat.dev accepted the delete. The computer was kept fenced; retry Delete to reconcile it"), { status: 503, cause: error });
274
274
  }
275
275
  if (boxDeleteProvedAbsent(removed)) {
276
276
  finishRecordedBoxDeletion(identity.boxId);
@@ -305,12 +305,12 @@ export async function boxNameFor(botId) {
305
305
  export async function boxNameMatchesBot(botId, name) {
306
306
  return name === await boxNameFor(botId) || name === legacyBoxNameFor(botId);
307
307
  }
308
- export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000 } = {}) {
308
+ export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000, signal } = {}) {
309
309
  assertBoxNotDeleting(boxId);
310
310
  const res = await boxFetch(cfg, `/boxes/${boxId}/commands`, {
311
311
  method: "POST",
312
312
  body: JSON.stringify({ command }),
313
- signal: AbortSignal.timeout(timeoutMs),
313
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
314
314
  });
315
315
  const body = await res.json().catch(() => null);
316
316
  return {
@@ -367,12 +367,12 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
367
367
  const boxIdCache = new Map();
368
368
  function boxInventoryProblem(status, body) {
369
369
  if (status === 401 || status === 403) {
370
- return "ascii.dev rejected the Box API key — update it in Settings → Connections";
370
+ return "boat.dev rejected the Box API key — update it in Settings → Connections";
371
371
  }
372
372
  if (status === 429)
373
- return "ascii.dev is rate-limiting this account — wait a minute and refresh";
373
+ return "boat.dev is rate-limiting this account — wait a minute and refresh";
374
374
  const message = typeof body?.message === "string" ? body.message.trim() : "";
375
- return message ? `ascii.dev could not list cloud computers: ${message}` : `ascii.dev could not list cloud computers (${status})`;
375
+ return message ? `boat.dev could not list cloud computers: ${message}` : `boat.dev could not list cloud computers (${status})`;
376
376
  }
377
377
  function safeBoxState(value) {
378
378
  if (typeof value !== "string")
@@ -391,7 +391,7 @@ async function listBoxPages(cfg) {
391
391
  listed = await boxJson(cfg, path, { signal: AbortSignal.timeout(20_000) });
392
392
  }
393
393
  catch {
394
- return { ok: false, problem: "Could not reach ascii.dev to list cloud computers — check your connection and refresh" };
394
+ return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
395
395
  }
396
396
  if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
397
397
  return { ok: false, problem: boxInventoryProblem(listed.status, listed.body), credentialRejected: listed.status === 401 || listed.status === 403 };
@@ -401,15 +401,15 @@ async function listBoxPages(cfg) {
401
401
  if (next === undefined || next === null || next === "")
402
402
  return { ok: true, boxes };
403
403
  if (typeof next !== "string" || next.length > 4_096) {
404
- return { ok: false, problem: "ascii.dev returned an invalid cloud computer page cursor" };
404
+ return { ok: false, problem: "boat.dev returned an invalid cloud computer page cursor" };
405
405
  }
406
406
  if (seenCursors.has(next)) {
407
- return { ok: false, problem: "ascii.dev repeated a cloud computer page cursor — refresh and try again" };
407
+ return { ok: false, problem: "boat.dev repeated a cloud computer page cursor — refresh and try again" };
408
408
  }
409
409
  seenCursors.add(next);
410
410
  cursor = next;
411
411
  }
412
- return { ok: false, problem: "ascii.dev returned too many cloud computer pages — narrow the account inventory and refresh" };
412
+ return { ok: false, problem: "boat.dev returned too many cloud computer pages — narrow the account inventory and refresh" };
413
413
  }
414
414
  /**
415
415
  * One account listing for Settings and deletion guards. Only boxes
@@ -469,12 +469,12 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
469
469
  continue;
470
470
  const matchingRows = candidates.filter((candidate) => candidate?.id === recovery.boxId);
471
471
  if (matchingRows.length > 1) {
472
- return invalidInventory("ascii.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
472
+ return invalidInventory("boat.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
473
473
  }
474
474
  if (matchingRows.length === 1) {
475
475
  const listedName = typeof matchingRows[0]?.name === "string" ? matchingRows[0].name : "";
476
476
  if (listedName !== namedOwner.currentName && listedName !== namedOwner.legacyName) {
477
- return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in ascii.dev before continuing");
477
+ return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in boat.dev before continuing");
478
478
  }
479
479
  continue;
480
480
  }
@@ -490,7 +490,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
490
490
  }
491
491
  if (inspected.identity.name !== namedOwner.currentName
492
492
  && inspected.identity.name !== namedOwner.legacyName) {
493
- return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in ascii.dev before continuing");
493
+ return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in boat.dev before continuing");
494
494
  }
495
495
  const directCandidate = {
496
496
  id: inspected.identity.boxId,
@@ -522,11 +522,11 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
522
522
  }
523
523
  const matchingRows = candidates.filter((candidate) => candidate?.id === deletion.boxId);
524
524
  if (matchingRows.length > 1) {
525
- return invalidInventory("ascii.dev returned a conflicting id for a cloud computer being deleted");
525
+ return invalidInventory("boat.dev returned a conflicting id for a cloud computer being deleted");
526
526
  }
527
527
  if (matchingRows.length === 1) {
528
528
  if (matchingRows[0]?.name !== deletion.name) {
529
- return invalidInventory("A cloud computer being deleted no longer has its remembered name — repair it in ascii.dev before continuing");
529
+ return invalidInventory("A cloud computer being deleted no longer has its remembered name — repair it in boat.dev before continuing");
530
530
  }
531
531
  if (getBoxDeletion(deletion.boxId)?.phase === "accepted") {
532
532
  matchingRows[0] = { ...matchingRows[0], state: "removing" };
@@ -574,11 +574,11 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
574
574
  continue;
575
575
  const boxId = typeof candidate.id === "string" ? candidate.id : "";
576
576
  if (!BOX_ID.test(boxId)) {
577
- return invalidInventory("ascii.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
577
+ return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
578
578
  }
579
579
  const existing = ownedBoxByBot.get(owner.botId);
580
580
  if (existing && existing !== boxId) {
581
- return invalidInventory("ascii.dev returned conflicting cloud computers for one OpenMaus bot — repair them in ascii.dev before continuing");
581
+ return invalidInventory("boat.dev returned conflicting cloud computers for one OpenMaus bot — repair them in boat.dev before continuing");
582
582
  }
583
583
  ownedBoxByBot.set(owner.botId, boxId);
584
584
  }
@@ -602,7 +602,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
602
602
  else if (LEGACY_MANAGED_BOX_NAME.test(name)) {
603
603
  // Pre-scope names have no installation provenance. A live local bot is
604
604
  // the only safe ownership proof; unmatched legacy rows stay provider-
605
- // managed until the person handles them in ascii.dev directly.
605
+ // managed until the person handles them in boat.dev directly.
606
606
  owner = ownerByLegacyName.get(name) ?? null;
607
607
  if (!owner)
608
608
  continue;
@@ -615,17 +615,17 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
615
615
  // deterministic name), silently skipping a malformed/duplicated identity
616
616
  // could let bot deletion mistake provider corruption for absence.
617
617
  if (!BOX_ID.test(boxId)) {
618
- return invalidInventory("ascii.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
618
+ return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
619
619
  }
620
620
  if ((boxIdCounts.get(boxId) ?? 0) !== 1 || seenBoxIds.has(boxId)) {
621
- return invalidInventory("ascii.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
621
+ return invalidInventory("boat.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
622
622
  }
623
623
  if (legacyOwner && owner && options.adoptLegacy !== false) {
624
624
  try {
625
625
  adoptResolvedBox(owner.botId, boxId);
626
626
  }
627
627
  catch {
628
- return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in ascii.dev before continuing");
628
+ return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in boat.dev before continuing");
629
629
  }
630
630
  }
631
631
  seenBoxIds.add(boxId);
@@ -663,7 +663,7 @@ export async function inspectBoxIdentity(cfg, boxId) {
663
663
  return {
664
664
  available: false,
665
665
  identity: null,
666
- problem: "Could not reach ascii.dev to verify a remembered cloud computer",
666
+ problem: "Could not reach boat.dev to verify a remembered cloud computer",
667
667
  };
668
668
  }
669
669
  if (inspected.status === 404 || inspected.status === 410) {
@@ -676,7 +676,7 @@ export async function inspectBoxIdentity(cfg, boxId) {
676
676
  const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
677
677
  const name = typeof candidate?.name === "string" ? candidate.name : "";
678
678
  if (returnedId !== boxId || name.length === 0 || name.length > 100 || /[\r\n]/.test(name)) {
679
- return { available: false, identity: null, problem: "ascii.dev returned an invalid cloud computer identity" };
679
+ return { available: false, identity: null, problem: "boat.dev returned an invalid cloud computer identity" };
680
680
  }
681
681
  return { available: true, identity: { boxId, name, state: safeBoxState(candidate.state) }, problem: null };
682
682
  }
@@ -717,7 +717,7 @@ const QUIESCE_BROWSER = [
717
717
  async function stopBox(cfg, boxId) {
718
718
  assertBoxNotDeleting(boxId);
719
719
  // Browser shutdown is best-effort, but the provider stop is not: Settings
720
- // must never say a computer is sleeping when ascii.dev rejected the action.
720
+ // must never say a computer is sleeping when boat.dev rejected the action.
721
721
  await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
722
722
  const stopped = await boxJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
723
723
  if (!stopped.ok)
@@ -753,7 +753,7 @@ export async function sleepManagedBox(cfg, owners, boxId, claim) {
753
753
  }
754
754
  }
755
755
  /** Permanent Settings action. The caller must echo the exact freshly-listed
756
- * machine name as well as its id; ascii.dev independently requires the id in
756
+ * machine name as well as its id; boat.dev independently requires the id in
757
757
  * its confirmation header. */
758
758
  export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim, options = {}) {
759
759
  cfg = snapshotBoxConfig(cfg);
@@ -812,7 +812,7 @@ export async function findBox(cfg, botId) {
812
812
  if (direct?.ok && directBox?.id === cachedId && directBox.state !== "error")
813
813
  return directBox;
814
814
  if (direct?.ok && directBox?.id !== cachedId) {
815
- throw Object.assign(new Error("ascii.dev returned an invalid cloud computer identity"), { status: 503 });
815
+ throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
816
816
  }
817
817
  boxIdCache.delete(botId); // gone or broken — fall back to the listing
818
818
  }
@@ -826,7 +826,7 @@ export async function findBox(cfg, botId) {
826
826
  // discoverable only for this exact local bot id.
827
827
  const expected = listed.boxes.filter((candidate) => candidate?.name === name || candidate?.name === legacyName);
828
828
  if (expected.some((candidate) => !BOX_ID.test(candidate?.id))) {
829
- throw Object.assign(new Error("ascii.dev returned an invalid cloud computer identity"), { status: 503 });
829
+ throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
830
830
  }
831
831
  const found = expected.find((candidate) => candidate.name === name && candidate.state !== "error")
832
832
  ?? expected.find((candidate) => candidate.name === legacyName && candidate.state !== "error")
@@ -834,7 +834,7 @@ export async function findBox(cfg, botId) {
834
834
  if (found) {
835
835
  const duplicateId = listed.boxes.filter((candidate) => candidate?.id === found.id).length !== 1;
836
836
  if (duplicateId) {
837
- throw Object.assign(new Error("ascii.dev returned a conflicting cloud computer identity"), { status: 503 });
837
+ throw Object.assign(new Error("boat.dev returned a conflicting cloud computer identity"), { status: 503 });
838
838
  }
839
839
  if (found.name === legacyName)
840
840
  adoptResolvedBox(botId, found.id);
@@ -872,14 +872,14 @@ export async function verifyToken(token) {
872
872
  return {
873
873
  ok: false,
874
874
  message: token.startsWith("box_")
875
- ? "ascii.dev rejected that token — it may have been revoked or expired. Copy a fresh one from your ascii.dev account."
876
- : "That doesn't look like a box API key: they start with box_. Copy the API key from your ascii.dev account (an account or session token won't work here).",
875
+ ? "boat.dev rejected that token — it may have been revoked or expired. Copy a fresh one from your boat.dev account."
876
+ : "That doesn't look like a box API key: they start with box_. Copy the API key from your boat.dev account (an account or session token won't work here).",
877
877
  };
878
878
  }
879
- return { ok: false, message: `ascii.dev returned ${res.status} for that token — try again in a moment.` };
879
+ return { ok: false, message: `boat.dev returned ${res.status} for that token — try again in a moment.` };
880
880
  }
881
881
  catch {
882
- return { ok: false, message: "Couldn't reach ascii.dev to check that token — check your connection and retry." };
882
+ return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
883
883
  }
884
884
  }
885
885
  /** Turn a provider refusal into something a person can act on. The
@@ -891,17 +891,17 @@ export function boxErrorMessage(status, what, body) {
891
891
  const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
892
892
  if (status === 402) {
893
893
  // e.g. "Start the $20/month Box plan to create sandboxes."
894
- return [theirs || "ascii.dev needs a paid Box plan before it will create a computer.", link].filter(Boolean).join(" ");
894
+ return [theirs || "boat.dev needs a paid Box plan before it will create a computer.", link].filter(Boolean).join(" ");
895
895
  }
896
896
  if (status === 401 || status === 403) {
897
- return "your box token was rejected by ascii.dev — open App Settings and paste a current token (it starts with box_)";
897
+ return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
898
898
  }
899
899
  if (status === 429) {
900
- return theirs || "ascii.dev is rate-limiting this account — wait a minute and try again";
900
+ return theirs || "boat.dev is rate-limiting this account — wait a minute and try again";
901
901
  }
902
902
  return theirs ? `${what} failed: ${theirs}` : `${what} failed (${status})`;
903
903
  }
904
- /** ascii.dev trial accounts reject the normal eight-hour auto-stop with a
904
+ /** boat.dev trial accounts reject the normal eight-hour auto-stop with a
905
905
  * structured `trial_auto_stop_required` refusal. Retry that one condition
906
906
  * once at the provider's advertised maximum (or the documented two-hour
907
907
  * trial ceiling). Other create failures must retain their original error. */
@@ -922,7 +922,7 @@ function idempotentCreateInProgress(result) {
922
922
  }
923
923
  /** The keys this OpenMausBot already holds, as the environment its bots'
924
924
  * agents read on the box. The box is created with `noEnv: true`, so the
925
- * ascii.dev account's own logins never land in the guest: the box has exactly
925
+ * boat.dev account's own logins never land in the guest: the box has exactly
926
926
  * these and nothing else (see "Whose keys" in the Box integrated-agents docs). */
927
927
  export function boxCredentialEnv(cfg, env = process.env) {
928
928
  const out = {};
@@ -991,7 +991,7 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
991
991
  });
992
992
  }
993
993
  catch (error) {
994
- // A dropped response is ambiguous: ascii.dev may already have created
994
+ // A dropped response is ambiguous: boat.dev may already have created
995
995
  // the Box. One retry with the same key recovers it safely.
996
996
  if (ambiguousRetries++ === 0)
997
997
  continue;
@@ -1084,7 +1084,7 @@ export async function provisionBox(cfg, botId, _botName) {
1084
1084
  assertBotBoxNotDeleting(botId);
1085
1085
  // Provider-side backstop: archives itself (billing pauses, disk
1086
1086
  // survives) if every stop path dies. Trial accounts get one narrower
1087
- // retry when ascii.dev reports their shorter TTL ceiling.
1087
+ // retry when boat.dev reports their shorter TTL ceiling.
1088
1088
  const createRes = await createBox(cfg, botId, credentialEnv);
1089
1089
  if (!createRes.ok || !createRes.body?.box?.id) {
1090
1090
  throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
@@ -1134,13 +1134,13 @@ export async function provisionBox(cfg, botId, _botName) {
1134
1134
  }
1135
1135
  catch (cleanupError) {
1136
1136
  const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
1137
- throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in ascii.dev.`);
1137
+ throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in boat.dev.`);
1138
1138
  }
1139
1139
  if (cleanupConfirmation === "confirmed")
1140
1140
  throw error;
1141
1141
  boxIdCache.delete(botId);
1142
- throw Object.assign(new Error(`${originalMessage}. ascii.dev accepted deletion of the new computer, but it is still pending; `
1143
- + `its recovery record was kept, along with its deletion fence. Check box ${box.id} in ascii.dev.`, { cause: error }), { status: 503 });
1142
+ throw Object.assign(new Error(`${originalMessage}. boat.dev accepted deletion of the new computer, but it is still pending; `
1143
+ + `its recovery record was kept, along with its deletion fence. Check box ${box.id} in boat.dev.`, { cause: error }), { status: 503 });
1144
1144
  }
1145
1145
  }
1146
1146
  /** Wake the bot's box and return a FRESH desktop URL. */
@@ -1216,16 +1216,16 @@ export const PANEL_FRAME_QUALITY = 85;
1216
1216
  // ffmpeg's -q:v runs 2 (best) to 31; 3 lands near JPEG quality 85.
1217
1217
  const PANEL_FRAME_FFMPEG_Q = 3;
1218
1218
  /** The shell that captures one panel frame on the box. Exported for tests. */
1219
- export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FRAME_QUALITY } = {}) {
1219
+ export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FRAME_QUALITY, framePath = PANEL_PATH, nativeSize = false } = {}) {
1220
1220
  return [
1221
1221
  "export DISPLAY=${DISPLAY:-:0}",
1222
- `f=${PANEL_PATH}`,
1222
+ `f=${shellQuote(framePath)}`,
1223
1223
  // a stale frame must not pass `test -s` when every capture tool fails
1224
1224
  'rm -f "$f"',
1225
1225
  'w=$(xdotool getdisplaygeometry 2>/dev/null | cut -d" " -f1)',
1226
1226
  'case "$w" in ""|*[!0-9]*) w=0;; esac',
1227
1227
  `scrot -o -p -q ${quality} "$f" 2>/dev/null || import -window root -quality ${quality} "$f" 2>/dev/null || ffmpeg -y -f x11grab -draw_mouse 1 -i "$DISPLAY" -frames:v 1 -q:v ${PANEL_FRAME_FFMPEG_Q} "$f" >/dev/null 2>&1`,
1228
- `if [ "$w" -gt ${width} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -resize ${width}x -quality ${quality} "$f" 2>/dev/null || true; fi`,
1228
+ ...(nativeSize ? [] : [`if [ "$w" -gt ${width} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -resize ${width}x -quality ${quality} "$f" 2>/dev/null || true; fi`]),
1229
1229
  'test -s "$f" && echo captured',
1230
1230
  ].join("; ");
1231
1231
  }
@@ -1237,11 +1237,11 @@ const MAX_FRAME_BYTES = 8 * 1024 * 1024;
1237
1237
  const FRAME_TOO_LARGE = "the box frame exceeds the 8 MB limit";
1238
1238
  /** Read a file off the box as base64 — raw artifact bytes when the API
1239
1239
  * supports it (33% less transfer, no JSON envelope), else the files API. */
1240
- async function readFileBase64(cfg, boxId, path) {
1240
+ async function readFileBase64(cfg, boxId, path, signal) {
1241
1241
  let bytes = null;
1242
1242
  let tooLarge = false;
1243
1243
  try {
1244
- const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`);
1244
+ const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`, { signal });
1245
1245
  if (res.ok) {
1246
1246
  const declaredLength = res.headers.get("content-length");
1247
1247
  if (declaredLength !== null && Number(declaredLength) > MAX_FRAME_BYTES)
@@ -1258,7 +1258,8 @@ async function readFileBase64(cfg, boxId, path) {
1258
1258
  }
1259
1259
  if (bytes?.length)
1260
1260
  return bytes.toString("base64");
1261
- const { ok, body } = await boxJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`);
1261
+ signal?.throwIfAborted();
1262
+ const { ok, body } = await boxJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`, { signal });
1262
1263
  const content = body?.content;
1263
1264
  if (ok && typeof content === "string" && content) {
1264
1265
  if (Buffer.byteLength(content, "base64") > MAX_FRAME_BYTES)
@@ -1269,7 +1270,7 @@ async function readFileBase64(cfg, boxId, path) {
1269
1270
  }
1270
1271
  /** `knownBoxId` skips box resolution entirely — the screen poller holds
1271
1272
  * the id for the whole turn and must not re-resolve it every frame. */
1272
- export async function screenshotBox(cfg, botId, knownBoxId) {
1273
+ export async function screenshotBox(cfg, botId, knownBoxId, options) {
1273
1274
  cfg = snapshotBoxConfig(cfg);
1274
1275
  let boxId = knownBoxId;
1275
1276
  if (!boxId) {
@@ -1280,11 +1281,12 @@ export async function screenshotBox(cfg, botId, knownBoxId) {
1280
1281
  throw new Error(`box is ${box.state}`);
1281
1282
  boxId = box.id;
1282
1283
  }
1283
- const out = await runCommand(cfg, boxId, SHOT_CMD, { timeoutMs: 60_000 });
1284
+ const framePath = options?.nativeSize ? PANEL_PATH + ".model.jpg" : PANEL_PATH;
1285
+ const out = await runCommand(cfg, boxId, options?.nativeSize ? panelShotCommand({ framePath, nativeSize: true }) : SHOT_CMD, { timeoutMs: 60_000, signal: options?.signal });
1284
1286
  if (!/captured/.test(out.stdout)) {
1285
1287
  throw new Error(out.stderr.slice(0, 200) || "screen capture failed on the box");
1286
1288
  }
1287
- const data = await readFileBase64(cfg, boxId, PANEL_PATH);
1289
+ const data = await readFileBase64(cfg, boxId, framePath, options?.signal);
1288
1290
  if (!data)
1289
1291
  throw new Error("could not read the frame back from the box");
1290
1292
  return { png: data, format: "jpeg" };
@@ -69,8 +69,9 @@ export async function browserProxyRequest(frame, connection) {
69
69
  return { jsonrpc: "2.0", id, result: payload.result };
70
70
  }
71
71
  catch (error) {
72
- return failure(id, message.method, error instanceof Error && !/fetch failed|abort|timeout/i.test(error.message)
73
- ? error.message : "Browser connection was interrupted. Do not repeat the action until its state is checked.");
72
+ const detail = error instanceof Error && !/fetch failed|abort|timeout/i.test(error.message)
73
+ ? error.message : "Browser connection was interrupted. Do not repeat the action until its state is checked.";
74
+ return failure(id, message.method, detail + " This failure does not establish that all browsers are unavailable. Inspect available connections with select_computer if that tool is present. Keep the requested computer and account; do not bypass a permission refusal or human takeover, and do not repeat an uncertain action without checking its result.");
74
75
  }
75
76
  }
76
77
  function run() {
@@ -279,11 +279,47 @@ export class BrowserRuntime {
279
279
  // The model sees slimmed schemas and text-only, bounded results; the
280
280
  // launch/session parameters OMB owns never reach the engine from a call.
281
281
  const request = method === "tools/call" ? stripHarnessOwnedArguments(params) : params;
282
- const result = await entry.client.rpc(method, request);
282
+ let result = await entry.client.rpc(method, request);
283
283
  beforeDispatch?.(); // A turn revoked while the tool ran receives no result.
284
284
  if (method === "tools/list")
285
285
  return slimBrowserToolList(result);
286
286
  const toolName = request && typeof request === "object" && typeof request.name === "string" ? request.name : undefined;
287
+ if (toolName === "agent_browser_open" && result && typeof result === "object" &&
288
+ result.isError !== true) {
289
+ // Navigation alone does not prove that the requested page loaded.
290
+ // Observe in the same scoped session, without replaying the action.
291
+ beforeDispatch?.();
292
+ if (this.gate(session).owner !== null)
293
+ throw new Error(BROWSER_CONTROL_REFUSAL);
294
+ let observation;
295
+ try {
296
+ observation = await entry.client.rpc("tools/call", {
297
+ name: "agent_browser_snapshot", arguments: { compact: true },
298
+ });
299
+ }
300
+ catch (error) {
301
+ // A tool-level refusal does not undo the completed navigation.
302
+ // Transport failures still engage the uncertainty/recovery gate.
303
+ if (error instanceof TransportError)
304
+ throw error;
305
+ observation = { isError: true, content: [] };
306
+ }
307
+ beforeDispatch?.();
308
+ const navigation = result;
309
+ const page = observation;
310
+ const observed = page?.isError !== true && Array.isArray(page?.content) && page.content.some((item) => item && typeof item === "object" && item.type === "text" &&
311
+ typeof item.text === "string" && item.text.trim().length > 0);
312
+ result = {
313
+ content: [
314
+ ...(Array.isArray(navigation.content) ? navigation.content : []),
315
+ { type: "text", text: !observed
316
+ ? "Navigation returned, but page verification failed. Do not claim the requested page loaded and do not blindly repeat navigation."
317
+ : "Page observed after navigation. Check this result for redirects, sign-in requirements or page errors before reporting task success:" },
318
+ ...(Array.isArray(page?.content) ? page.content : []),
319
+ ],
320
+ ...(!observed ? { isError: true } : {}),
321
+ };
322
+ }
287
323
  return shapeBrowserToolResult(result, { toolName, budget: this.options.resultBudget });
288
324
  }
289
325
  catch (error) {
@@ -4,6 +4,7 @@ export const API_ENDPOINTS = [
4
4
  { id: "openai", label: "OpenAI API", url: "https://api.openai.com/v1", keyUrl: "https://platform.openai.com/api-keys" },
5
5
  { id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyUrl: "https://openrouter.ai/settings/keys" },
6
6
  { id: "groq", label: "Groq", url: "https://api.groq.com/openai/v1", keyUrl: "https://console.groq.com/keys" },
7
+ { id: "requesty", label: "Requesty", url: "https://router.requesty.ai/v1", keyUrl: "https://app.requesty.ai/api-keys" },
7
8
  ];
8
9
  export function normalizeApiUrl(value) {
9
10
  const input = value.trim();
@@ -251,7 +251,7 @@ fleet many client workspaces on one Linux server, each its own account,
251
251
  --dry-run always prints. Install the package permanently first.
252
252
  init --operator USER also installs the fleet agent, a root service
253
253
  on a Unix socket only USER may open, so the workspace running as
254
- USER manages the others from Settings → Workspaces.
254
+ USER manages the others from Settings → Installations.
255
255
 
256
256
  --tailscale serve over your tailnet: Tailscale terminates HTTPS and the
257
257
  link uses this machine's MagicDNS name (needs Tailscale signed in
@@ -402,7 +402,7 @@ function startupPreferences(options) {
402
402
  async function showPhonePairing(options, origin, log) {
403
403
  const ready = !!origin && await verifyPhoneEndpoint(options.port, origin);
404
404
  if (!ready) {
405
- log("Phone access is not reachable yet. Your local workspace is ready; no phone pairing code was created.");
405
+ log("Phone access is not reachable yet. OpenMausBot is ready on this computer; no phone pairing code was created.");
406
406
  log("Check the HTTPS connection, then run openmausbot pair again with the same --data-dir and --port.");
407
407
  return false;
408
408
  }
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, sep } from "node:path";
4
+ import { writeFileAtomic } from "./atomic.js";
5
+ import { redactSecretsInText } from "./redact.js";
6
+ const MAX_COMMAND_BYTES = 16_384;
7
+ const MAX_CWD_BYTES = 4_096;
8
+ const MAX_RULES_PER_BOT = 200;
9
+ const MAX_STORE_BYTES = 8 * 1024 * 1024;
10
+ const BOT_ID = /^[\w-]{1,128}$/;
11
+ const RULE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
12
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
+ const invalid = (message) => Object.assign(new Error(message), { status: 400 });
14
+ /** Validate without rewriting the command. Resolving a symlink, or folding
15
+ * `..` across one, could make a grant apply in a different directory. */
16
+ export function validateCommandAllowlistCandidate(value) {
17
+ if (!record(value))
18
+ throw invalid("A command, absolute working directory and engine instance are required.");
19
+ const { command, cwd, providerInstanceId } = value;
20
+ if (typeof command !== "string" || !command.trim() || Buffer.byteLength(command) > MAX_COMMAND_BYTES ||
21
+ /[\p{Cc}\p{Cs}]/u.test(command.replace(/[\t\r\n]/g, ""))) {
22
+ throw invalid("The command must be nonempty text of at most 16384 bytes, without invalid control characters.");
23
+ }
24
+ if (redactSecretsInText(command) !== command) {
25
+ throw invalid("Commands containing credentials cannot be saved. Use an environment variable instead.");
26
+ }
27
+ if (typeof cwd !== "string" || !cwd || Buffer.byteLength(cwd) > MAX_CWD_BYTES ||
28
+ /[\p{Cc}\p{Cs}]/u.test(cwd) || !isAbsolute(cwd) || cwd.split(sep === "\\" ? /[\\/]/ : /\//).includes("..")) {
29
+ throw invalid("The working directory must be an absolute path without parent-directory segments.");
30
+ }
31
+ // On Windows, `\work` is rooted but still depends on the current drive.
32
+ if (process.platform === "win32" && !/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+(?:[\\/]|$))/.test(cwd)) {
33
+ throw invalid("The working directory must include its drive or network share.");
34
+ }
35
+ if (typeof providerInstanceId !== "string" || !providerInstanceId || providerInstanceId.trim() !== providerInstanceId ||
36
+ providerInstanceId.length > 200 || /[\p{Cc}\p{Cs}]/u.test(providerInstanceId)) {
37
+ throw invalid("A valid engine instance is required.");
38
+ }
39
+ // Keep directory spelling exact too. A false negative is preferable to
40
+ // treating two paths as equivalent without knowing the engine's filesystem.
41
+ return { command, cwd, providerInstanceId };
42
+ }
43
+ /** Runtime metadata that cannot identify an exact scope never auto-allows. */
44
+ export function commandAllowlistCandidate(value) {
45
+ try {
46
+ return validateCommandAllowlistCandidate(value);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function validateBotId(botId) {
53
+ if (typeof botId !== "string" || !BOT_ID.test(botId))
54
+ throw invalid("A valid bot id is required.");
55
+ }
56
+ function sameScope(left, right) {
57
+ return left.command === right.command && left.cwd === right.cwd && left.providerInstanceId === right.providerInstanceId;
58
+ }
59
+ /** Installation-local grants, deliberately separate from bot profiles and
60
+ * templates. Callers must also exclude this file from workspace backups.
61
+ * One server owns the store; a successful atomic save precedes publication. */
62
+ export class CommandAllowlistStore {
63
+ rules = [];
64
+ unreadable = false;
65
+ filename;
66
+ constructor(filename) {
67
+ this.filename = filename;
68
+ try {
69
+ const stat = lstatSync(filename);
70
+ if (!stat.isFile() || stat.size > MAX_STORE_BYTES)
71
+ throw new Error("Invalid command rule store.");
72
+ const saved = JSON.parse(readFileSync(filename, "utf8"));
73
+ if (!record(saved) || saved.version !== 1 || !Array.isArray(saved.rules) ||
74
+ Object.keys(saved).some((key) => key !== "version" && key !== "rules"))
75
+ throw new Error("Invalid command rule store.");
76
+ const rules = [];
77
+ const ids = new Set();
78
+ const counts = new Map();
79
+ for (const row of saved.rules) {
80
+ if (!record(row) || typeof row.id !== "string" || !RULE_ID.test(row.id) || ids.has(row.id) ||
81
+ typeof row.botId !== "string" || Object.keys(row).some((key) => !["id", "botId", "command", "cwd", "providerInstanceId"].includes(key))) {
82
+ throw new Error("Invalid command rule store.");
83
+ }
84
+ validateBotId(row.botId);
85
+ const candidate = validateCommandAllowlistCandidate(row);
86
+ const count = (counts.get(row.botId) ?? 0) + 1;
87
+ if (count > MAX_RULES_PER_BOT)
88
+ throw new Error("Too many saved command rules.");
89
+ counts.set(row.botId, count);
90
+ ids.add(row.id);
91
+ rules.push({ ...candidate, id: row.id, botId: row.botId });
92
+ }
93
+ this.rules = rules;
94
+ }
95
+ catch (error) {
96
+ // Missing means no grants. Any other read/validation failure disables
97
+ // all grants and forbids overwriting evidence of a damaged store.
98
+ if (error.code !== "ENOENT")
99
+ this.unreadable = true;
100
+ }
101
+ }
102
+ list(botId) {
103
+ validateBotId(botId);
104
+ return this.rules.filter((rule) => rule.botId === botId).map(({ botId: _botId, ...rule }) => rule);
105
+ }
106
+ add(botId, value) {
107
+ this.assertWritable();
108
+ validateBotId(botId);
109
+ const candidate = validateCommandAllowlistCandidate(value);
110
+ const existing = this.list(botId);
111
+ const duplicate = existing.find((rule) => sameScope(rule, candidate));
112
+ if (duplicate)
113
+ return duplicate;
114
+ if (existing.length >= MAX_RULES_PER_BOT)
115
+ throw invalid("A bot may save at most 200 commands.");
116
+ const rule = { ...candidate, id: randomUUID() };
117
+ this.save([...this.rules, { ...rule, botId }]);
118
+ return rule;
119
+ }
120
+ remove(botId, ruleId) {
121
+ this.assertWritable();
122
+ validateBotId(botId);
123
+ const next = this.rules.filter((rule) => rule.botId !== botId || rule.id !== ruleId);
124
+ if (next.length === this.rules.length)
125
+ return false;
126
+ this.save(next);
127
+ return true;
128
+ }
129
+ clear(botId) {
130
+ this.assertWritable();
131
+ validateBotId(botId);
132
+ const next = this.rules.filter((rule) => rule.botId !== botId);
133
+ if (next.length !== this.rules.length)
134
+ this.save(next);
135
+ }
136
+ matches(botId, value) {
137
+ if (this.unreadable || typeof botId !== "string" || !BOT_ID.test(botId))
138
+ return false;
139
+ const candidate = commandAllowlistCandidate(value);
140
+ return candidate !== null && this.rules.some((rule) => rule.botId === botId && sameScope(rule, candidate));
141
+ }
142
+ assertWritable() {
143
+ if (this.unreadable)
144
+ throw Object.assign(new Error("Saved command rules could not be read. Repair the rule store before changing them."), { status: 503 });
145
+ }
146
+ save(rules) {
147
+ const json = JSON.stringify({ version: 1, rules }, null, 2);
148
+ if (Buffer.byteLength(json) > MAX_STORE_BYTES)
149
+ throw invalid("The saved command rules exceed the storage limit.");
150
+ mkdirSync(dirname(this.filename), { recursive: true, mode: 0o700 });
151
+ writeFileAtomic(this.filename, json, { mode: 0o600 });
152
+ this.rules = rules;
153
+ }
154
+ }