openmausbot 0.1.95 → 0.1.97

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 (169) hide show
  1. package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +26 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/container-mcp.js +14 -27
  64. package/dist-server/drivers/agents-proxy.js +108 -49
  65. package/dist-server/harness-mcp-proxy.js +1 -2
  66. package/dist-server/index.js +4664 -4644
  67. package/dist-server/openmausbot.js +1425 -1437
  68. package/dist-server/pair-cli.js +1425 -1437
  69. package/dist-server/permission-proxy.js +0 -4
  70. package/dist-server/server/admin-activity.js +1 -4
  71. package/dist-server/server/auto-approve.js +20 -5
  72. package/dist-server/server/auto-vm-claims.js +13 -1
  73. package/dist-server/server/automatic-recovery.js +1 -2
  74. package/dist-server/server/boat.js +71 -87
  75. package/dist-server/server/bot-overview.js +1 -1
  76. package/dist-server/server/browser-engine.js +38 -1
  77. package/dist-server/server/browser-live.js +112 -25
  78. package/dist-server/server/browser-runtime.js +31 -1
  79. package/dist-server/server/checkpoints.js +340 -167
  80. package/dist-server/server/chief-of-staff.js +7 -6
  81. package/dist-server/server/cli-setup.js +3 -3
  82. package/dist-server/server/cli.js +33 -26
  83. package/dist-server/server/cloud-computer-tools.js +11 -4
  84. package/dist-server/server/cloud-home-start.js +13 -16
  85. package/dist-server/server/cloud-home.js +3 -13
  86. package/dist-server/server/cloud-move-http.js +44 -30
  87. package/dist-server/server/cloud-move.js +20 -18
  88. package/dist-server/server/cloud-owner.js +1 -1
  89. package/dist-server/server/composio.js +10 -3
  90. package/dist-server/server/computer-engine-removal.js +70 -0
  91. package/dist-server/server/computer-selection.js +18 -0
  92. package/dist-server/server/config.js +99 -75
  93. package/dist-server/server/connector-scope-hint.js +83 -0
  94. package/dist-server/server/container-computer.js +16 -5
  95. package/dist-server/server/contracts.js +0 -9
  96. package/dist-server/server/decider/jev.js +1 -4
  97. package/dist-server/server/decider/log.js +1 -3
  98. package/dist-server/server/decision-log.js +1 -4
  99. package/dist-server/server/default-model-selection.js +20 -14
  100. package/dist-server/server/delegations.js +14 -7
  101. package/dist-server/server/delta-context.js +6 -1
  102. package/dist-server/server/drivers/acp/core.js +5 -4
  103. package/dist-server/server/drivers/acp/opencode-go.js +19 -23
  104. package/dist-server/server/drivers/agents-call.js +47 -64
  105. package/dist-server/server/drivers/agents-catalog.js +69 -3
  106. package/dist-server/server/drivers/agents-proxy.js +2 -1
  107. package/dist-server/server/drivers/builtIn.js +0 -2
  108. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  109. package/dist-server/server/drivers/claude.js +49 -30
  110. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  111. package/dist-server/server/drivers/codex.js +7 -6
  112. package/dist-server/server/drivers/grok.js +1 -1
  113. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  114. package/dist-server/server/drivers/openai-chat.js +113 -11
  115. package/dist-server/server/drivers/pi.js +184 -49
  116. package/dist-server/server/drivers/prompt-split.js +4 -4
  117. package/dist-server/server/harness/bus.js +49 -0
  118. package/dist-server/server/harness-mcp-proxy.js +1 -1
  119. package/dist-server/server/index.js +942 -980
  120. package/dist-server/server/live-call.js +11 -6
  121. package/dist-server/server/managed-policy.js +1 -1
  122. package/dist-server/server/mcp-registry.js +35 -0
  123. package/dist-server/server/message-file.js +4 -4
  124. package/dist-server/server/model-catalog/presets.js +8 -0
  125. package/dist-server/server/peer-delivery.js +4 -19
  126. package/dist-server/server/peer-roster.js +40 -6
  127. package/dist-server/server/profile-versions.js +0 -31
  128. package/dist-server/server/provider-key-check.js +2 -1
  129. package/dist-server/server/restart.js +40 -0
  130. package/dist-server/server/room-handoffs.js +12 -0
  131. package/dist-server/server/room-turn-end.js +15 -0
  132. package/dist-server/server/routes/bot-memory.js +0 -24
  133. package/dist-server/server/routes/live.js +3 -3
  134. package/dist-server/server/routine-requests.js +2 -5
  135. package/dist-server/server/routines.js +6 -5
  136. package/dist-server/server/screen-frame-gate.js +1 -3
  137. package/dist-server/server/server-launcher.js +53 -0
  138. package/dist-server/server/store.js +126 -2
  139. package/dist-server/server/surface.js +40 -64
  140. package/dist-server/server/system-prompt.js +23 -13
  141. package/dist-server/server/team-manifest.js +1 -6
  142. package/dist-server/server/tunnel.js +4 -1
  143. package/dist-server/server/turn-continuation.js +162 -0
  144. package/dist-server/server/turn-images.js +1 -21
  145. package/dist-server/server/turn-resources.js +19 -150
  146. package/dist-server/server/usage-ledger.js +3 -1
  147. package/dist-server/server/workspace.js +3 -4
  148. package/dist-server/server-launcher.js +72 -0
  149. package/dist-server/shared/ask-question.js +1 -71
  150. package/dist-server/shared/cloud-computer.js +10 -0
  151. package/dist-server/shared/credential-request.js +1 -1
  152. package/dist-server/shared/failed-turn.js +22 -0
  153. package/dist-server/shared/live-approval.js +2 -2
  154. package/dist-server/shared/package-format.js +3 -3
  155. package/dist-server/shared/pairing-link.js +144 -0
  156. package/dist-server/shared/place-view.js +409 -0
  157. package/dist-server/shared/tool-scope-support.js +1 -2
  158. package/dist-server/vps-container-mcp.js +14 -27
  159. package/dist-server/workspace-backup.worker.js +14 -27
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DS2NbT5x.js +0 -1
  162. package/dist/assets/index-NF7XCHAy.css +0 -1
  163. package/dist/assets/index-X_5ydXIE.js +0 -610
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
  165. package/dist-server/server/claim-idle.js +0 -33
  166. package/dist-server/server/cloud-overflow.js +0 -167
  167. package/dist-server/server/drivers/boatagent.js +0 -443
  168. package/dist-server/server/tightening-requests.js +0 -435
  169. package/dist-server/shared/tightening-request.js +0 -87
@@ -8,7 +8,6 @@ var MAX_OPTIONS = 12;
8
8
  var MAX_QUESTION_TEXT = 8e3;
9
9
  var MAX_LABEL = 120;
10
10
  var MAX_DESCRIPTION = 400;
11
- var MAX_CUSTOM_ANSWER = 2e3;
12
11
  function text(value, limit) {
13
12
  if (typeof value !== "string") return void 0;
14
13
  const trimmed = value.trim();
@@ -60,9 +59,6 @@ function parseAskQuestions(input) {
60
59
  }
61
60
  return questions.length ? questions : null;
62
61
  }
63
- var OMB_ASK_FENCE = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]{0,3}\2[ \t]*(?=\r?\n|$)/;
64
- var OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
65
- var MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
66
62
  function questionAnswersByQuestion(message, questions) {
67
63
  return questionAnswersById(message, questions.map((question) => ({ id: question.question, question })));
68
64
  }
@@ -20,16 +20,13 @@ import { appendFile, mkdir } from "node:fs/promises";
20
20
  import { basename, join } from "node:path";
21
21
  import { actorLabel, boundRetentionDays, pruneMonthFiles } from "./decision-log.js";
22
22
  import { redactSecrets } from "./redact.js";
23
- import { csvCell } from "./usage-ledger.js";
23
+ import { csvCell, monthKey } from "./usage-ledger.js";
24
24
  export const ADMIN_ACTIVITY_CATEGORIES = ["config", "people", "session", "webhook", "mcp", "engine", "bot", "budget", "visibility"];
25
25
  const DIR = "admin-activity";
26
26
  const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
27
27
  const PRUNE_EVERY_MS = 60 * 60_000;
28
28
  const writeQueues = new Map();
29
29
  const lastPrune = new Map();
30
- function monthKey(at) {
31
- return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
32
- }
33
30
  export function adminActivityFileFor(dataDir, at) {
34
31
  return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
35
32
  }
@@ -3,9 +3,11 @@
3
3
  // Nothing here decides whether an action is safe. Each approval level is a
4
4
  // provider's own permission mode passed straight through (Claude `auto`,
5
5
  // Grok `--permission-mode`, Codex `approvalsReviewer`, …), and a request that
6
- // reaches this process is one the provider left for a person. The only
7
- // grants the app applies are Full access and the person's exact saved commands.
8
- // Questions never come through here: a bot's question always reaches a human.
6
+ // reaches this process is one the provider left for a person. The grants the
7
+ // app applies are Full access and the person's exact saved commands. Approve
8
+ // for me also allows a web search, and that is the one action the app itself
9
+ // grants. Questions never come through here: a bot's question always reaches
10
+ // a human.
9
11
  import { supportsApprovalMode } from "../shared/approval-mode.js";
10
12
  import { isOutboundTool } from "../shared/outbound.js";
11
13
  /** A failed delivery is a runtime error, not another permission decision.
@@ -77,12 +79,20 @@ export function delegatedApprovalMode(input) {
77
79
  // the permission path in permission-proxy). Approving it there does not
78
80
  // produce an answer: the CLI runs the tool with none and the model is told
79
81
  // "The user did not answer the questions." — a question silently lost.
80
- const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user", "omb-ask"]);
82
+ const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user"]);
83
+ // A web search is a low-level read. Approve for me allows these names and
84
+ // nothing wider: a bare search, a page fetch, and every other tool stay a
85
+ // prompt. The name is the same shape as a question — one mcp__<server>__
86
+ // prefix removed, then compared case-insensitively.
87
+ const WEB_SEARCH = new Set(["websearch", "web_search", "web-search", "search_web"]);
88
+ function bareToolName(tool) {
89
+ return tool.replace(/^mcp__[^_]+__/, "").toLowerCase();
90
+ }
81
91
  export function autoVerdict(mode, tool, context) {
82
92
  // A question is for a person, whatever channel it arrived on — and
83
93
  // whatever the mode: even Full has no answer to give, only an approval
84
94
  // that would run the tool with none.
85
- if (ASKS_A_PERSON.has(tool.replace(/^mcp__[^_]+__/, "").toLowerCase())) {
95
+ if (ASKS_A_PERSON.has(bareToolName(tool))) {
86
96
  return { approve: null, source: "no-grant" };
87
97
  }
88
98
  // Full's promise is literal: even a sandbox widening is approved. Entering
@@ -96,6 +106,11 @@ export function autoVerdict(mode, tool, context) {
96
106
  return { approve: null, source: "outbound-guard" };
97
107
  if (context?.commandAllowed)
98
108
  return { approve: `approved ${tool} (saved command)`, source: "command-allowlist" };
109
+ // The one action the app itself grants. A fetch of an arbitrary URL is not
110
+ // a search and stays a prompt, in this mode and every other.
111
+ if (mode === "auto" && WEB_SEARCH.has(bareToolName(tool))) {
112
+ return { approve: `approved ${tool} (web search)`, source: "web-search" };
113
+ }
99
114
  if (mode === "auto" || mode === "custom")
100
115
  return { approve: null, source: "native-approval" };
101
116
  return { approve: null, source: "no-grant" };
@@ -18,6 +18,18 @@ export function startAutoVmClaim(table, threadId, generation) {
18
18
  slot.begin = slot.claim().then(() => { slot.claimed = true; }, (error) => {
19
19
  slot.failed = true;
20
20
  slot.failure = error instanceof Error ? error.message : String(error);
21
- slot.onRejected?.(slot.failure);
21
+ slot.onRejected?.(slot.failure, error);
22
22
  });
23
23
  }
24
+ /** How long a computer call should wait on its thread's lazy claim, or null
25
+ * when it should not wait: the first call waits while the claim it fires
26
+ * lands; a slow claim (graceMs) is waited on by every call until it settles. */
27
+ export function lazyClaimWaitMs(slot, defaultGraceMs) {
28
+ if (!slot?.lazy)
29
+ return null;
30
+ if (!slot.begin)
31
+ return slot.graceMs ?? defaultGraceMs;
32
+ if (slot.graceMs !== undefined && !slot.claimed && !slot.failed)
33
+ return slot.graceMs;
34
+ return null;
35
+ }
@@ -3,8 +3,7 @@ import { supportsWorkspaceFiles } from "./workspace.js";
3
3
  * its filesystem to another machine. Approval and organisation policy are
4
4
  * checked by the ordinary model-switch and turn-admission gates. */
5
5
  export function recoveryCapabilityError(from, to) {
6
- if (from.capabilities.remoteAgent || to.capabilities.remoteAgent ||
7
- supportsWorkspaceFiles(from.driverKind) !== supportsWorkspaceFiles(to.driverKind)) {
6
+ if (supportsWorkspaceFiles(from.driverKind) !== supportsWorkspaceFiles(to.driverKind)) {
8
7
  return "The backup cannot use the same workspace.";
9
8
  }
10
9
  const tools = ["agentsMcp", "computerMcp", "composioMcp",
@@ -86,15 +86,12 @@ function scopedBoatPrefix() {
86
86
  scopedBoatPrefixCache = `ogb-${scope}-`;
87
87
  return scopedBoatPrefixCache;
88
88
  }
89
- /** Decide lifecycle work before a turn mounts Boat. Auto may observe and
90
- * attach an already-ready Boat, but only explicit Cloud may create or wake. */
91
- export function boatTurnLifecycleAction({ explicitCloud, canMount, state, }) {
92
- if (!canMount)
93
- return "none";
89
+ /** Decide lifecycle work before a turn mounts Boat. Only a turn whose place
90
+ * is Cloud gets here (Auto never reads the Boat account), so a missing Boat
91
+ * is created and a sleeping one woken. */
92
+ export function boatTurnLifecycleAction(state) {
94
93
  if (state && READY.has(state))
95
94
  return "attach";
96
- if (!explicitCloud)
97
- return "none";
98
95
  return state ? "wake" : "provision";
99
96
  }
100
97
  /** Keep one provider account for the whole logical operation. Settings may
@@ -348,36 +345,46 @@ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
348
345
  }
349
346
  async function waitReady(cfg, boxId, budgetMs = 90_000) {
350
347
  assertBoatNotDeleting(boxId);
351
- const t0 = Date.now();
348
+ const deadline = Date.now() + budgetMs;
349
+ // Every request ends with the budget: a relay that accepts the connection
350
+ // and then stalls must not hold a turn's start past it.
351
+ const untilDeadline = () => AbortSignal.timeout(Math.max(1, deadline - Date.now()));
352
+ const outOfTime = (error) => error instanceof Error && error.name === "TimeoutError";
352
353
  // Boat's words for the last failed resume, if the wait runs out on them.
353
354
  let resumeFailure = null;
354
- while (Date.now() - t0 < budgetMs) {
355
- assertBoatNotDeleting(boxId);
356
- const { body } = await boatJson(cfg, `/boxes/${boxId}`);
357
- const state = body?.box?.state;
358
- if (READY.has(state))
359
- return body.box;
360
- if (state === "error")
361
- return null;
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;
355
+ try {
356
+ while (Date.now() < deadline) {
357
+ assertBoatNotDeleting(boxId);
358
+ const { body } = await boatJson(cfg, `/boxes/${boxId}`, { signal: untilDeadline() });
359
+ const state = body?.box?.state;
360
+ if (READY.has(state))
361
+ return body.box;
362
+ if (state === "error")
363
+ return null;
364
+ // an archiving boat can't resume until the snapshot lands — nudge after.
365
+ // A refusal (a plan limit, say) is final: report it now. A server error
366
+ // is retried on the next poll, as Boat asks; 409 is a state race with a
367
+ // wake already under way.
368
+ if (state === "archived") {
369
+ const resumed = await boatJson(cfg, `/boxes/${boxId}/resume`, { method: "POST", signal: untilDeadline() });
370
+ if (resumed.ok)
371
+ resumeFailure = null;
372
+ else if (resumed.status !== 409) {
373
+ const refusal = boatRefusal(resumed.status, "waking the cloud computer", resumed.body, usesIncludedBoat(cfg));
374
+ if (resumed.status < 500)
375
+ throw refusal;
376
+ resumeFailure = refusal;
377
+ }
375
378
  }
379
+ await new Promise((r) => setTimeout(r, Math.min(2500, Math.max(0, deadline - Date.now()))));
376
380
  }
377
- await new Promise((r) => setTimeout(r, 2500));
381
+ }
382
+ catch (error) {
383
+ if (!outOfTime(error))
384
+ throw error;
378
385
  }
379
386
  if (resumeFailure)
380
- throw new Error(resumeFailure);
387
+ throw resumeFailure;
381
388
  return null;
382
389
  }
383
390
  // Resolving a bot's boat means LISTing every boat in the account, so it is
@@ -828,7 +835,9 @@ export async function findBoat(cfg, botId) {
828
835
  if (cachedId) {
829
836
  let direct = null;
830
837
  try {
831
- direct = await boatJson(cfg, `/boxes/${cachedId}`);
838
+ // Bounded like every other Boat read: a relay that accepts the
839
+ // connection and stalls must not hold a turn's setup for minutes.
840
+ direct = await boatJson(cfg, `/boxes/${cachedId}`, { signal: AbortSignal.timeout(20_000) });
832
841
  }
833
842
  catch {
834
843
  // A direct read can fail while the account listing still succeeds.
@@ -948,6 +957,17 @@ export function boatErrorMessage(status, what, body, included = false) {
948
957
  }
949
958
  return theirs ? `${what} failed: ${theirs}` : `${what} failed (${status})`;
950
959
  }
960
+ /** A refused start as an error that keeps the provider's status and code
961
+ * (the Admin's own, such as subscription_inactive), so a failed place is
962
+ * read from the code first and from the words only until every refusal
963
+ * has one (shared/place-view.ts cloudRefusal). Named apart from `status`,
964
+ * which a route would answer with. */
965
+ export function boatRefusal(status, what, body, included = false) {
966
+ const code = body?.error?.code ?? body?.code;
967
+ return Object.assign(new Error(boatErrorMessage(status, what, body, included)), {
968
+ boatStatus: status, ...(typeof code === "string" && /^[a-z0-9_]{1,64}$/.test(code) ? { boatCode: code } : {}),
969
+ });
970
+ }
951
971
  /** boat.dev trial accounts reject the normal eight-hour auto-stop with a
952
972
  * structured `trial_auto_stop_required` refusal. Retry that one condition
953
973
  * once at the provider's advertised maximum (or the documented two-hour
@@ -967,44 +987,13 @@ function idempotentCreateInProgress(result) {
967
987
  const code = result.body?.error?.code ?? result.body?.code;
968
988
  return result.status === 409 && code === "idempotency_in_progress";
969
989
  }
970
- /** The keys this OpenMausBot already holds, as the environment its bots'
971
- * agents read on the boat. The boat is created with `noEnv: true`, so the
972
- * boat.dev account's own logins never land in the guest: the boat has exactly
973
- * these and nothing else (see "Whose keys" in the Boat integrated-agents docs). */
974
- export function boatCredentialEnv(cfg, env = process.env) {
975
- const out = {};
976
- const put = (name, value) => {
977
- if (typeof value === "string" && value.trim())
978
- out[name] = value.trim();
979
- };
980
- // The workspace key only: an ANTHROPIC_API_KEY in the server's own env is
981
- // never the workspace key (see loadConfig), so it is not forwarded either.
982
- put("ANTHROPIC_API_KEY", cfg.anthropic?.key);
983
- for (const name of BOAT_FORWARDED_CREDENTIAL_ENV)
984
- put(name, env[name]);
985
- return out;
986
- }
987
- /** Names the boat's agents read (Claude Code, Codex, pi, OpenCode, Prime
988
- * Agent, Kimi), forwarded verbatim from this server's environment when set. */
989
- const BOAT_FORWARDED_CREDENTIAL_ENV = [
990
- "CLAUDE_CODE_OAUTH_TOKEN",
991
- "OPENAI_API_KEY",
992
- "OPENROUTER_API_KEY",
993
- "LLMGATEWAY_API_KEY",
994
- "DEEPSEEK_API_KEY",
995
- "MOONSHOT_API_KEY",
996
- "KIMI_CODE_ACCESS_TOKEN",
997
- "KIMI_CODE_REFRESH_TOKEN",
998
- ];
999
- async function requestBoatCreate(cfg, botId, ttlSeconds, env) {
1000
- // The computer needs the user's desktop session, not the account owner's
1001
- // host credentials. Keep provider-side env injection off; the only keys the
1002
- // guest ever has are the ones this OpenMausBot forwards (`env`), which its
1003
- // agents need now that the turn runs on the boat. The idempotency identity
1004
- // stays the secret-free part: a trial-TTL retry must receive a different
1005
- // key, and the journal on disk never carries a credential.
990
+ async function requestBoatCreate(cfg, botId, ttlSeconds) {
991
+ // The computer is a desktop the bot's own engine drives from here, so it
992
+ // needs no AI sign-in of its own: provider-side env injection stays off and
993
+ // no key this OpenMausBot holds is ever sent to it. A trial-TTL retry must
994
+ // receive a different idempotency key, and the journal on disk never
995
+ // carries a credential.
1006
996
  const body = JSON.stringify({ ttlSeconds, noEnv: true });
1007
- const wireBody = JSON.stringify({ ttlSeconds, noEnv: true, ...(Object.keys(env).length ? { env } : {}) });
1008
997
  let attempt = beginBoatCreate(botId, body);
1009
998
  let request = attempt.request;
1010
999
  let createdThisAttempt = attempt.startedNow;
@@ -1034,7 +1023,7 @@ async function requestBoatCreate(cfg, botId, ttlSeconds, env) {
1034
1023
  method: "POST",
1035
1024
  headers: { "Idempotency-Key": request.idempotencyKey },
1036
1025
  signal: AbortSignal.timeout(45_000),
1037
- body: wireBody,
1026
+ body,
1038
1027
  });
1039
1028
  }
1040
1029
  catch (error) {
@@ -1067,12 +1056,12 @@ async function requestBoatCreate(cfg, botId, ttlSeconds, env) {
1067
1056
  return { ...last, request, createdThisAttempt };
1068
1057
  }
1069
1058
  }
1070
- async function createBoat(cfg, botId, env) {
1071
- const first = await requestBoatCreate(cfg, botId, DEFAULT_BOAT_TTL_SECONDS, env);
1059
+ async function createBoat(cfg, botId) {
1060
+ const first = await requestBoatCreate(cfg, botId, DEFAULT_BOAT_TTL_SECONDS);
1072
1061
  if (first.ok)
1073
1062
  return first;
1074
1063
  const trialTtl = trialBoatTtlSeconds(first.body);
1075
- return trialTtl === null ? first : requestBoatCreate(cfg, botId, trialTtl, env);
1064
+ return trialTtl === null ? first : requestBoatCreate(cfg, botId, trialTtl);
1076
1065
  }
1077
1066
  /** A prior explicit delete always wins over provisioning. Reconcile/retry the
1078
1067
  * old immutable target, then require a fresh provision request so one click
@@ -1111,10 +1100,9 @@ export async function boatStatus(cfg, botId) {
1111
1100
  }
1112
1101
  /**
1113
1102
  * Find-or-create the bot's persistent boat, wait for ready, and mint a fresh
1114
- * desktop URL. The boat ships its own computer-use driver and agent runner.
1103
+ * desktop URL. The boat ships its own computer-use driver.
1115
1104
  */
1116
1105
  export async function provisionBoat(cfg, botId, _botName) {
1117
- const credentialEnv = boatCredentialEnv(cfg);
1118
1106
  cfg = snapshotBoatConfig(cfg);
1119
1107
  if (!boatConfigured(cfg)) {
1120
1108
  throw new Error('box provider not enabled — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
@@ -1132,9 +1120,9 @@ export async function provisionBoat(cfg, botId, _botName) {
1132
1120
  // Provider-side backstop: archives itself (billing pauses, disk
1133
1121
  // survives) if every stop path dies. Trial accounts get one narrower
1134
1122
  // retry when boat.dev reports their shorter TTL ceiling.
1135
- const createRes = await createBoat(cfg, botId, credentialEnv);
1123
+ const createRes = await createBoat(cfg, botId);
1136
1124
  if (!createRes.ok || !createRes.body?.box?.id) {
1137
- throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg)));
1125
+ throw boatRefusal(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg));
1138
1126
  }
1139
1127
  boat = createRes.body.box;
1140
1128
  createRequest = createRes.request;
@@ -1151,8 +1139,7 @@ export async function provisionBoat(cfg, botId, _botName) {
1151
1139
  const ready = await waitReady(cfg, boat.id);
1152
1140
  if (!ready)
1153
1141
  throw new Error("box did not become ready within 90s — retry in a minute");
1154
- // Nothing to install: every boat ships its own computer-use driver and
1155
- // registers it with every harness it runs.
1142
+ // Nothing to install: every boat ships its own computer-use driver.
1156
1143
  const joinUrl = await mintDesktopUrl(cfg, boat.id);
1157
1144
  if (!joinUrl)
1158
1145
  throw new Error("box desktop link could not be created");
@@ -1249,14 +1236,11 @@ export async function execOnBoat(cfg, botId, command) {
1249
1236
  //
1250
1237
  // The frame is for a person: it fills the panel and opens in the chat's
1251
1238
  // image viewer, so it keeps the desktop's native size up to 1080p and a
1252
- // quality where page text stays legible. (Sizing it is now the only say
1253
- // OpenMausBot has over any frame off this box: the turn runs on the boat's
1254
- // own agent, so the model's own captures never pass through here.) Only
1255
- // wider displays are scaled down, with -resize rather than -thumbnail so
1256
- // the resample is not the fast-and-blurry kind meant for icons. The
1257
- // pointer is drawn into the frame (scrot --pointer, ffmpeg -draw_mouse):
1258
- // watching the bot work means seeing where its cursor is, and X11
1259
- // captures leave it out by default.
1239
+ // quality where page text stays legible. Only wider displays are scaled
1240
+ // down, with -resize rather than -thumbnail so the resample is not the
1241
+ // fast-and-blurry kind meant for icons. The pointer is drawn into the frame
1242
+ // (scrot --pointer, ffmpeg -draw_mouse): watching the bot work means seeing
1243
+ // where its cursor is, and X11 captures leave it out by default.
1260
1244
  const PANEL_PATH = "/tmp/ogb-panel.jpg";
1261
1245
  export const PANEL_FRAME_WIDTH = 1920;
1262
1246
  export const PANEL_FRAME_QUALITY = 85;
@@ -147,7 +147,7 @@ export function grantsSummary(connectorTools) {
147
147
  function computerReach(computer, cloudHome = false) {
148
148
  // A Cloud home never offers either; say so rather than list a preference
149
149
  // that every task there refuses.
150
- const unavailable = cloudHome ? ", which isn't available on OMB Cloud" : "";
150
+ const unavailable = cloudHome ? ", which isn't available on My Cloud" : "";
151
151
  switch (computer) {
152
152
  case "cloud":
153
153
  return "Computer preference: cloud computer.";
@@ -16,7 +16,7 @@ import { delimiter, dirname, join, resolve } from "node:path";
16
16
  import { writeFileAtomic } from "./atomic.js";
17
17
  import { DATA_DIR } from "./config.js";
18
18
  import { browserBundlePaths } from "./browser-bundle-release.js";
19
- import { browserRuntimeEnv } from "./browser-runtime.js";
19
+ import { BROWSER_VIEWPORT_ARGS, browserRuntimeEnv, ownsBrowserViewport } from "./browser-runtime.js";
20
20
  import { SIGN_IN_PROMPT } from "./system-prompt.js";
21
21
  import { AGENT_BROWSER_VERSION, agentBrowserReleaseUrl, agentBrowserReleaseVersion, resolveAgentBrowserReleaseAsset, } from "./browser-engine-release.js";
22
22
  const ENGINE_DIR = "tools/agent-browser";
@@ -467,6 +467,43 @@ export function agentBrowserIntegration(input) {
467
467
  }
468
468
  return { command: input.binaryPath, args: ["mcp", "--tools", "core", "--no-webmcp"], env };
469
469
  }
470
+ /** Give the session's browser the standard page size, launching it with the
471
+ * session's own launch environment if it is not running yet. Best effort:
472
+ * false (and the page keeps whatever size it had) on any failure. */
473
+ export function setBrowserViewport(binaryPath, env, timeoutMs = 30_000) {
474
+ if (!ownsBrowserViewport(env))
475
+ return Promise.resolve(true);
476
+ return new Promise((settle) => {
477
+ let child;
478
+ try {
479
+ child = spawn(binaryPath, [...BROWSER_VIEWPORT_ARGS, "--json", "--no-webmcp"], { env: browserRuntimeEnv(env), stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
480
+ }
481
+ catch {
482
+ settle(false);
483
+ return;
484
+ }
485
+ let output = "";
486
+ let settled = false;
487
+ const finish = (ok) => { if (!settled) {
488
+ settled = true;
489
+ clearTimeout(timer);
490
+ settle(ok);
491
+ } };
492
+ const timer = setTimeout(() => { child.kill("SIGKILL"); finish(false); }, timeoutMs);
493
+ timer.unref?.();
494
+ child.stdout?.on("data", (chunk) => { if (output.length < 65_536)
495
+ output += String(chunk); });
496
+ child.on("error", () => finish(false));
497
+ child.on("close", (code) => {
498
+ try {
499
+ finish(code === 0 && JSON.parse(output)?.success === true);
500
+ }
501
+ catch {
502
+ finish(false);
503
+ }
504
+ });
505
+ });
506
+ }
470
507
  /** How long a settled-frame capture may take before the turn gives up on it.
471
508
  * The poller runs beside a live turn, so a hung browser must not hold the
472
509
  * transcript open; a missing picture is better than a stuck fold. */