openmausbot 0.1.94 → 0.1.96

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 (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. package/dist-server/shared/tightening-request.js +0 -87
@@ -0,0 +1,110 @@
1
+ import { isolatedRemoteCommand, runCommand, screenshotBoat } from "./boat.js";
2
+ import { CONTROL_REFUSAL_PLAIN } from "./control-client.js";
3
+ import { compileToolSchema } from "./drivers/chat-mcp-tools.js";
4
+ const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
5
+ const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
6
+ export const CLOUD_COMPUTER_TOOLS = [
7
+ tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
8
+ tool("get_screen_size", "Get the cloud desktop width and height."),
9
+ tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
10
+ tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
11
+ tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
12
+ tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
13
+ tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
14
+ tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
15
+ tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
16
+ tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
17
+ ];
18
+ const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
19
+ const buttons = { left: 1, middle: 2, right: 3 };
20
+ const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
21
+ let validators;
22
+ /** Why a call cannot run, or null. Checked before anything else touches the
23
+ * call: x and y reach the remote shell unquoted, so only the advertised
24
+ * integer schema stands between a model's arguments and a command line. */
25
+ export function cloudComputerCallProblem(name, args) {
26
+ validators ??= new Map(CLOUD_COMPUTER_TOOLS.map(entry => [entry.name, compileToolSchema(entry.inputSchema)]));
27
+ const validate = typeof name === "string" ? validators.get(name) : undefined;
28
+ if (!validate)
29
+ return "Unknown cloud computer tool. Use one of the advertised computer tools.";
30
+ if (!args || typeof args !== "object" || Array.isArray(args) || !validate(args)) {
31
+ return "Tool arguments do not match the advertised input schema; use its required fields and types.";
32
+ }
33
+ return null;
34
+ }
35
+ /** One tool call on the given Boat with the server's own Boat account. The
36
+ * caller has already checked the arguments and the person's control. */
37
+ export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
38
+ const problem = cloudComputerCallProblem(name, args);
39
+ if (problem)
40
+ return { isError: true, content: [{ type: "text", text: problem }] };
41
+ signal.throwIfAborted();
42
+ if (name === "screenshot") {
43
+ const shot = await screenshotBoat(cfg, "", boxId, { signal, nativeSize: true });
44
+ signal.throwIfAborted();
45
+ return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
46
+ }
47
+ const a = args;
48
+ const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
49
+ let command;
50
+ switch (name) {
51
+ case "get_screen_size":
52
+ command = "xdotool getdisplaygeometry";
53
+ break;
54
+ case "click":
55
+ command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
56
+ break;
57
+ case "move":
58
+ command = mouse;
59
+ break;
60
+ case "drag":
61
+ command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
62
+ break;
63
+ case "type_text":
64
+ command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
65
+ break;
66
+ case "key_press":
67
+ command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
68
+ break;
69
+ case "scroll":
70
+ command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
71
+ break;
72
+ case "open_url":
73
+ command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
74
+ break;
75
+ case "exec":
76
+ command = String(a.command);
77
+ break;
78
+ default: return { isError: true, content: [{ type: "text", text: "Unknown cloud computer tool." }] };
79
+ }
80
+ const result = await runCommand(cfg, boxId, isolatedRemoteCommand(command), { signal });
81
+ signal.throwIfAborted();
82
+ if (name === "open_url" && result.ok)
83
+ return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
84
+ return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
85
+ }
86
+ /** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
87
+ * `result`. The order is the safety argument: arguments are checked before
88
+ * the control gate (which may claim the seat), a person's control refuses
89
+ * the call before anything reaches the Boat, and the turn's capability must
90
+ * still be live before and after the Boat acts. */
91
+ export async function cloudComputerRpc(request, deps) {
92
+ if (request?.method === "tools/list") {
93
+ deps.assertActive();
94
+ return { tools: CLOUD_COMPUTER_TOOLS };
95
+ }
96
+ if (request?.method !== "tools/call")
97
+ throw Object.assign(new Error("unsupported computer method"), { status: 400 });
98
+ const params = (request.params && typeof request.params === "object" ? request.params : {});
99
+ const args = params.arguments ?? {};
100
+ const problem = cloudComputerCallProblem(params.name, args);
101
+ if (problem)
102
+ return { isError: true, content: [{ type: "text", text: problem }] };
103
+ const control = await deps.gate();
104
+ deps.assertActive();
105
+ if (control.held)
106
+ return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
107
+ const result = await runCloudComputerTool(deps.cfg, deps.boxId, params.name, args, deps.signal);
108
+ deps.assertActive();
109
+ return result;
110
+ }
@@ -8,7 +8,7 @@
8
8
  // forwards with X-Forwarded-*, so request-auth.ts never grants a remote
9
9
  // request loopback trust. If either child exits, both stop and the machine
10
10
  // restarts; the one exception is the server asking to be started again
11
- // after a restore.
11
+ // after a restore (server/restart.ts).
12
12
  //
13
13
  // The machine's secrets (the signing secret, the relay tokens) arrive as
14
14
  // this process's environment. No child's environment ever carries them:
@@ -24,7 +24,8 @@ import { spawn } from "node:child_process";
24
24
  import { chownSync, readFileSync, statSync } from "node:fs";
25
25
  import { dirname, join, posix, resolve } from "node:path";
26
26
  import { fileURLToPath, pathToFileURL } from "node:url";
27
- import { CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
27
+ import { CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
28
+ import { restartPolicy } from "./restart.js";
28
29
  const SERVICE_USER = "maus";
29
30
  /** The descriptor the server reads its secrets from. */
30
31
  const SECRETS_FD = 3;
@@ -112,11 +113,6 @@ export function spawnWithSecrets(command, args, env, secrets, ids) {
112
113
  pipe?.end(JSON.stringify(secrets));
113
114
  return child;
114
115
  }
115
- /** What the launcher does when the server child exits: start it again only
116
- * when it asked to (a committed restore), and only a few times in a row. */
117
- export function serverExitAction(code, stopping, restarts) {
118
- return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
119
- }
120
116
  export function startCloudHome(env = process.env) {
121
117
  process.umask(0o077);
122
118
  const config = cloudHomeConfiguration(env);
@@ -189,14 +185,15 @@ export function startCloudHome(env = process.env) {
189
185
  stop(true);
190
186
  });
191
187
  };
192
- let restarts = 0;
193
- const runServer = () => watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
194
- if (serverExitAction(code, stopping, restarts) !== "restart")
195
- return false;
196
- restarts++;
197
- runServer();
198
- return true;
199
- });
188
+ const policy = restartPolicy();
189
+ const runServer = () => {
190
+ watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
191
+ if (!policy.again(code, stopping))
192
+ return false;
193
+ runServer();
194
+ return true;
195
+ });
196
+ };
200
197
  runServer();
201
198
  watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
202
199
  process.once("SIGTERM", () => stop(false));
@@ -26,7 +26,6 @@ import { join } from "node:path";
26
26
  import { writeFileAtomic } from "./atomic.js";
27
27
  import { hostedWorkspaceConfigured } from "./enterprise.js";
28
28
  import { formatPairingCode } from "./sessions.js";
29
- export const CLOUD_HOME_CONTRACT_VERSION = 1;
30
29
  /** Any of these switches the server into Cloud home mode; then all are required. */
31
30
  export const CLOUD_HOME_KEYS = ["OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_BOOTSTRAP_SECRET"];
32
31
  /** A platform model gateway's settings. A Cloud home never uses them: given
@@ -46,10 +45,6 @@ export const CLOUD_PAIRING_SKEW_S = 300;
46
45
  export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
47
46
  const MAX_NONCES = 10_000;
48
47
  export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
49
- /** The server exits with this after a restore commits (Move to Cloud): the
50
- * launcher then starts it again, and startup installs the restore. Any other
51
- * exit stops the machine for Fly to restart. */
52
- export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
53
48
  export function cloudHomeConfigured(env = process.env) {
54
49
  return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
55
50
  }
@@ -21,9 +21,6 @@ import { existsSync, lstatSync, readFileSync } from "node:fs";
21
21
  import { z } from "zod";
22
22
  import { writeFileAtomic } from "./atomic.js";
23
23
  const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
24
- export function cloudHomeTurnMayLend(turn) {
25
- return cloudHomeLendingRefusal(turn) === null;
26
- }
27
24
  export function cloudHomeLendingRefusal(turn) {
28
25
  const { request } = turn;
29
26
  if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
@@ -1,9 +1,18 @@
1
1
  import { z } from "zod";
2
+ import { sharedSignIn } from "./admin-activity.js";
2
3
  import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, enabledRoutineCount, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, totalVolumeBytes, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
3
4
  import { commitPendingWorkspaceRestore, createWorkspaceBackup, stageWorkspaceBackup } from "./workspace-backup.js";
4
5
  export const CLOUD_MOVE_PREFIX = "/api/cloud-move";
5
6
  const failure = (message, status) => Object.assign(new Error(message), { status });
6
7
  const HELD_CACHE_MS = 30_000;
8
+ /** Shared with other people, so one person's copy must never replace it: a
9
+ * hosted organisation workspace, or (not on a Cloud home, whose sign-in is its
10
+ * owner's OMB Cloud account) an email sign-in list that lets someone else in
11
+ * (sharedSignIn: a member, a second admin, or a whole @domain). The owner's
12
+ * own address alone (`openmausbot access add you@example.com`) is not. */
13
+ export function workspaceShared(input) {
14
+ return input.hosted || (!input.cloudHome && sharedSignIn(input.signIn));
15
+ }
7
16
  export function createCloudMoveRoutes(options) {
8
17
  const freeBytes = options.freeBytes ?? freeVolumeBytes;
9
18
  const volumeBytes = (path) => { try {
@@ -12,16 +21,17 @@ export function createCloudMoveRoutes(options) {
12
21
  catch {
13
22
  return null;
14
23
  } };
15
- // A move's restore that startup has installed keeps no safety copy or
16
- // staged files; an abandoned upload goes after a day.
17
- if (options.cloudHome) {
18
- try {
19
- tidyCloudMoveStorage(options.dataDir, options.restored);
20
- }
21
- catch (error) {
22
- console.warn(`cloud move: could not tidy backup storage (${error instanceof Error ? error.message : "unknown"})`);
23
- }
24
+ // A copy's restore that startup has installed keeps no safety copy or
25
+ // staged files, and the workspace it replaced becomes the one to swap back
26
+ // to; an abandoned upload goes after a day. Only what a copy recorded is
27
+ // touched, so on a server that never received one this does nothing.
28
+ try {
29
+ tidyCloudMoveStorage(options.dataDir, options.restored);
30
+ }
31
+ catch (error) {
32
+ console.warn(`workspace copy: could not tidy backup storage (${error instanceof Error ? error.message : "unknown"})`);
24
33
  }
34
+ const identity = { appVersion: options.appVersion, environmentId: options.environmentId };
25
35
  // Staged here by a preview; restore accepts nothing else.
26
36
  const staged = new Set();
27
37
  let job = null;
@@ -33,16 +43,16 @@ export function createCloudMoveRoutes(options) {
33
43
  };
34
44
  const check = (req, auth) => {
35
45
  if (!options.authorized(req, auth))
36
- throw failure("Your session changed. Start the move again.", 403);
46
+ throw failure("Your session changed. Start the copy again.", 403);
37
47
  };
38
48
  const ready = () => {
39
49
  if (job?.state === "running" || writing)
40
- throw failure("Another move step is running on your Cloud. Wait for it to finish.", 409);
50
+ throw failure("Another copy step is running on this server. Wait for it to finish.", 409);
41
51
  const status = options.status();
42
52
  if (status.pendingRestore)
43
- throw failure("Your Cloud is restarting to finish a restore. Try again in a minute.", 409);
53
+ throw failure("This server is restarting to finish a restore. Try again in a minute.", 409);
44
54
  if (status.busy)
45
- throw failure("Your Cloud is busy with a backup. Try again when it finishes.", 409);
55
+ throw failure("This server is busy with a backup. Try again when it finishes.", 409);
46
56
  };
47
57
  /** Delete what earlier previews staged and no restore took. */
48
58
  const dropStaged = () => {
@@ -63,7 +73,7 @@ export function createCloudMoveRoutes(options) {
63
73
  after?.(result);
64
74
  }, (error) => {
65
75
  if (job === current)
66
- job = { kind, state: "failed", error: error instanceof Error ? error.message : "The move could not finish." };
76
+ job = { kind, state: "failed", error: error instanceof Error ? error.message : "The copy could not finish." };
67
77
  });
68
78
  }
69
79
  // A person's own page can hold the workspace gate for a moment (an ordinary
@@ -106,7 +116,7 @@ export function createCloudMoveRoutes(options) {
106
116
  const roomFor = (archive, already = 0) => {
107
117
  const needed = moveSpaceNeeded(archive, workspaceMoveSize(options.dataDir).bytes, true) - already;
108
118
  const free = freeBytes(options.dataDir);
109
- return free >= needed ? null : { error: "Your Cloud does not have enough free space for this.", freeBytes: free, neededBytes: needed };
119
+ return free >= needed ? null : { error: "This server does not have enough free space for this.", freeBytes: free, neededBytes: needed };
110
120
  };
111
121
  // The answer that the restore is ready goes out first; then the launcher
112
122
  // starts the server again, and startup installs the restore before anything else loads.
@@ -117,15 +127,16 @@ export function createCloudMoveRoutes(options) {
117
127
  const method = req.method ?? "GET";
118
128
  try {
119
129
  if (!auth.scopes.includes("admin"))
120
- throw failure("Only the owner can move a workspace.", 403);
130
+ throw failure("Only the owner can copy a workspace.", 403);
121
131
  if (method === "GET" && path === `${CLOUD_MOVE_PREFIX}/estimate`) {
122
- json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
132
+ json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES, ...identity });
123
133
  return true;
124
134
  }
125
- if (!options.cloudHome)
126
- throw failure("Only an OMB Cloud home receives a moved workspace.", 404);
127
135
  if (auth.kind !== "session")
128
- throw failure("Move to Cloud needs the owner's signed-in app.", 403);
136
+ throw failure("Copying a workspace needs the owner's signed-in app.", 403);
137
+ if (options.sharedWorkspace()) {
138
+ throw Object.assign(failure("This server is shared with other people, so it can't receive one computer's bots and chats. Copy to a server only you use.", 403), { code: "shared_workspace" });
139
+ }
129
140
  check(req, auth);
130
141
  if (method === "GET" && path === CLOUD_MOVE_PREFIX) {
131
142
  const contents = workspaceContents(options.dataDir);
@@ -134,8 +145,10 @@ export function createCloudMoveRoutes(options) {
134
145
  json(res, 200, {
135
146
  contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), volumeBytes: volumeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
136
147
  partBytes: CLOUD_MOVE_PART_BYTES, upload: uploadStatus(options.dataDir), previous: previousCloud(options.dataDir), job,
137
- // What backups and the previous Cloud hold on the volume.
148
+ // What backups and the previous workspace hold on the volume.
138
149
  heldBytes: held.bytes,
150
+ // The app refuses a server older than itself, or its own, before exporting.
151
+ ...identity,
139
152
  ...options.status(),
140
153
  lastRestoreId: options.restored.restored ? options.restored.id ?? null : null,
141
154
  rolledBackId: options.restored.rolledBack ? options.restored.id ?? null : null,
@@ -191,7 +204,7 @@ export function createCloudMoveRoutes(options) {
191
204
  }
192
205
  if (discardPreview) {
193
206
  removeMoveFiles(options.dataDir, result.id);
194
- throw failure("The move was stopped; what it staged was removed.", 409);
207
+ throw failure("The copy was stopped; what it staged was removed.", 409);
195
208
  }
196
209
  try {
197
210
  check(req, auth);
@@ -213,7 +226,7 @@ export function createCloudMoveRoutes(options) {
213
226
  if (method === "POST" && path === `${CLOUD_MOVE_PREFIX}/discard`) {
214
227
  await options.readBody(req, 4096);
215
228
  if (job?.state === "running" && job.kind !== "preview")
216
- throw failure("Your Cloud is already replacing its workspace.", 409);
229
+ throw failure("This server is already replacing its workspace.", 409);
217
230
  // A preview still running removes what it stages when it ends.
218
231
  if (job?.state === "running")
219
232
  discardPreview = true;
@@ -226,7 +239,7 @@ export function createCloudMoveRoutes(options) {
226
239
  const body = z.object({ id: z.string().uuid() }).parse(await options.readBody(req, 4096));
227
240
  ready();
228
241
  if (!staged.has(body.id))
229
- throw failure("This move is no longer ready on your Cloud. Start it again.", 404);
242
+ throw failure("This copy is no longer ready on this server. Start it again.", 404);
230
243
  start("restore", async () => {
231
244
  const { committed, previous } = await replaceWith(body.id, req, auth);
232
245
  return { kind: "restore", state: "done", id: committed.id, previous };
@@ -238,15 +251,15 @@ export function createCloudMoveRoutes(options) {
238
251
  await options.readBody(req, 4096);
239
252
  ready();
240
253
  if (!previousCloud(options.dataDir))
241
- throw failure("There is no previous Cloud to restore.", 404);
254
+ throw failure("There is nothing on this server to swap back to.", 404);
242
255
  const refused = roomFor(previousCloudArchiveBytes(options.dataDir));
243
256
  if (refused) {
244
257
  json(res, 507, refused);
245
258
  return true;
246
259
  }
247
260
  dropStaged();
248
- // A swap: what the Cloud has now becomes the previous Cloud, so this
249
- // can be undone the same way.
261
+ // A swap: what this server has now becomes the previous workspace, so
262
+ // this can be undone the same way.
250
263
  start("undo", async () => {
251
264
  const prepared = await stagePreviousCloud(options.dataDir, options.appVersion);
252
265
  const { committed, previous } = await replaceWith(prepared.id, req, auth);
@@ -255,7 +268,7 @@ export function createCloudMoveRoutes(options) {
255
268
  json(res, 202, { job });
256
269
  return true;
257
270
  }
258
- json(res, 404, { error: "Unknown move operation." });
271
+ json(res, 404, { error: "Unknown copy operation." });
259
272
  }
260
273
  catch (error) {
261
274
  if (res.headersSent) {
@@ -263,10 +276,11 @@ export function createCloudMoveRoutes(options) {
263
276
  return true;
264
277
  }
265
278
  const status = error instanceof z.ZodError ? 400 : error.status ?? 400;
266
- const received = error.received;
279
+ const received = error.received, code = error.code;
267
280
  json(res, status, {
268
- error: error instanceof z.ZodError ? "Invalid move request." : error instanceof Error ? error.message : "The move failed.",
281
+ error: error instanceof z.ZodError ? "Invalid copy request." : error instanceof Error ? error.message : "The copy failed.",
269
282
  ...(Number.isSafeInteger(received) ? { received } : {}),
283
+ ...(code === "shared_workspace" ? { code } : {}),
270
284
  });
271
285
  }
272
286
  return true;
@@ -1,16 +1,18 @@
1
- // Move to Cloud (docs/cloud-pro.md): a person's one-time copy of their local
2
- // workspace onto their OMB Cloud home. The desktop exports the ordinary
3
- // encrypted workspace backup (workspace-backup.ts) and uploads it here in
4
- // parts; this machine then previews and restores it like any other backup,
5
- // so everything that backup policy keeps out (credentials, sign-ins, pairing
6
- // and sessions, runtime state) stays where it is on both sides.
1
+ // Copy this computer here (docs/copy-workspace.md): a person's copy of their
2
+ // desktop's workspace onto a server they own and added in the desktop app, an
3
+ // OMB Cloud home included. The desktop exports the ordinary encrypted
4
+ // workspace backup (workspace-backup.ts) and uploads it here in parts; this
5
+ // server then previews and restores it like any other backup, so everything
6
+ // that backup policy keeps out (credentials, sign-ins, pairing and sessions,
7
+ // runtime state) stays where it is on both sides.
7
8
  //
8
9
  // What this file adds: the workspace's size and contents, a resumable upload
9
- // slot, the space check for the machine's fixed-size volume, a backup of the
10
- // Cloud's own workspace taken before it is replaced ("Restore previous Cloud"
11
- // swaps it back), and the tidying that keeps that backup the only copy left
12
- // behind. Everything lives under `.backups`, which no snapshot includes and
13
- // no restore replaces.
10
+ // slot, the space check for the server's volume, a backup of this server's
11
+ // own workspace taken before it is replaced (Swap back puts it back), and the
12
+ // tidying that keeps that backup the only copy left behind. Everything lives
13
+ // under `.backups`, which no snapshot includes and no restore replaces. The
14
+ // file and folder names still say "cloud" (`cloud-move`, `cloud-previous`):
15
+ // renaming them would orphan the swap back on servers already running.
14
16
  import { createHash, randomBytes } from "node:crypto";
15
17
  import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statfsSync, writeFileSync } from "node:fs";
16
18
  import { open } from "node:fs/promises";
@@ -167,9 +169,9 @@ export function validUploadDeclaration(input) {
167
169
  if (!Number.isSafeInteger(input.bytes) || input.bytes < MIN_BYTES)
168
170
  throw fail("This is not a workspace backup.", 400);
169
171
  if (input.bytes > CLOUD_MOVE_MAX_BYTES)
170
- throw fail("This workspace is larger than the 10 GB a move can carry.", 413);
172
+ throw fail("This workspace is larger than the 10 GB a copy can carry.", 413);
171
173
  if (input.files !== undefined && (!Number.isSafeInteger(input.files) || input.files < 0 || input.files > CLOUD_MOVE_MAX_FILES)) {
172
- throw fail("This workspace has more than the 100,000 files a move can carry.", 413);
174
+ throw fail("This workspace has more than the 100,000 files a copy can carry.", 413);
173
175
  }
174
176
  return { sha256: input.sha256, bytes: input.bytes };
175
177
  }
@@ -193,7 +195,7 @@ export function beginUpload(dataDir, declared, now = Date.now()) {
193
195
  export async function writeUploadPart(dataDir, sha256, offset, length, body, now = Date.now()) {
194
196
  const meta = readUploadMeta(dataDir);
195
197
  if (!meta || meta.sha256 !== sha256)
196
- throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
198
+ throw fail("This upload is not in progress on this server. Start the copy again.", 404);
197
199
  if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0 || length > CLOUD_MOVE_MAX_PART_BYTES) {
198
200
  throw fail("Invalid upload part.", 400);
199
201
  }
@@ -238,7 +240,7 @@ export async function writeUploadPart(dataDir, sha256, offset, length, body, now
238
240
  export async function completedUpload(dataDir, sha256) {
239
241
  const status = uploadStatus(dataDir);
240
242
  if (!status || status.sha256 !== sha256)
241
- throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
243
+ throw fail("This upload is not in progress on this server. Start the copy again.", 404);
242
244
  if (status.received !== status.bytes)
243
245
  throw fail("The upload is not complete yet.", 409, { received: status.received });
244
246
  const hash = createHash("sha256");
@@ -246,7 +248,7 @@ export async function completedUpload(dataDir, sha256) {
246
248
  hash.update(chunk);
247
249
  if (hash.digest("hex") !== sha256) {
248
250
  discardUpload(dataDir);
249
- throw fail("The uploaded file was damaged on the way. Start the move again.", 400);
251
+ throw fail("The uploaded file was damaged on the way. Start the copy again.", 400);
250
252
  }
251
253
  return uploadPartPath(dataDir);
252
254
  }
@@ -312,7 +314,7 @@ export function discardNextPreviousCloud(dataDir) {
312
314
  export async function stagePreviousCloud(dataDir, appVersion) {
313
315
  const previous = readPrevious(previousFolder(dataDir));
314
316
  if (!previous || isEmptyWorkspace(previous.contents))
315
- throw fail("There is no previous Cloud to restore.", 404);
317
+ throw fail("There is nothing on this server to swap back to.", 404);
316
318
  const archive = join(previousFolder(dataDir), "workspace.ombbackup");
317
319
  const staged = await stageWorkspaceBackup(dataDir, archive, { password: previous.password, currentAppVersion: appVersion });
318
320
  return { ...staged, bytes: lstatSync(archive).size };
@@ -373,7 +375,7 @@ function pendingRestoreId(dataDir) {
373
375
  return null;
374
376
  }
375
377
  }
376
- /** At a Cloud home's startup, after any pending restore was applied:
378
+ /** At every server's startup, after any pending restore was applied:
377
379
  * - a next previous Cloud becomes the previous Cloud once its restore is
378
380
  * installed, and is dropped otherwise;
379
381
  * - moves' and swaps' restores, once settled, keep no safety copy or staged
@@ -244,9 +244,35 @@ function trustedAuthUrl(value, slug) {
244
244
  }
245
245
  return url.toString();
246
246
  }
247
+ /** Composio's own hosts are always trusted. A backend the operator pointed
248
+ * the app at explicitly (OMB_COMPOSIO_API — a dev or test stub) may hand back
249
+ * a Session on its own origin, since the API itself was already trusted that
250
+ * far; any other host is refused. */
251
+ export function trustedSessionMcpUrl(value) {
252
+ let mcp;
253
+ try {
254
+ mcp = new URL(value);
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ if (mcp.protocol === "https:" && (mcp.hostname === "composio.dev" || mcp.hostname.endsWith(".composio.dev")))
260
+ return true;
261
+ if (mcp.protocol !== "https:" && !(mcp.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(mcp.hostname)))
262
+ return false;
263
+ const override = process.env.OMB_COMPOSIO_API;
264
+ if (!override)
265
+ return false;
266
+ try {
267
+ return new URL(override).origin === mcp.origin;
268
+ }
269
+ catch {
270
+ return false;
271
+ }
272
+ }
247
273
  function parseSessionResponse(session) {
248
274
  const mcp = new URL(session.mcp.url);
249
- if (mcp.protocol !== "https:" || (mcp.hostname !== "composio.dev" && !mcp.hostname.endsWith(".composio.dev"))) {
275
+ if (!trustedSessionMcpUrl(session.mcp.url)) {
250
276
  throw new Error("Composio returned an untrusted Session MCP URL");
251
277
  }
252
278
  return { ...session, mcp: { ...session.mcp, url: mcp.toString() } };
@@ -497,7 +523,7 @@ export async function mcpIntegration(cfg, context) {
497
523
  },
498
524
  };
499
525
  }
500
- export async function relayMcp(cfg, payload, transportSessionId) {
526
+ export async function relayMcp(cfg, payload, transportSessionId, beforeSend) {
501
527
  const apiKey = projectApiKey(cfg);
502
528
  let url;
503
529
  let identity;
@@ -529,6 +555,9 @@ export async function relayMcp(cfg, payload, transportSessionId) {
529
555
  if (forwardedTransportSessionId) {
530
556
  headers.set("mcp-session-id", forwardedTransportSessionId);
531
557
  }
558
+ // Session discovery can await network I/O. Turn authority and mutable
559
+ // permissions must be checked after it, immediately before dispatch.
560
+ beforeSend?.();
532
561
  const response = await fetch(url, {
533
562
  method: "POST",
534
563
  headers,
@@ -1077,7 +1106,7 @@ export async function removeAccount(cfg, slug, accountId) {
1077
1106
  }
1078
1107
  /** Mint a browser auth link for one service. Returns { url } or throws. */
1079
1108
  export async function authorizeService(cfg, slug, requestedAlias) {
1080
- const alias = normalizeAccountAlias(requestedAlias);
1109
+ let alias = normalizeAccountAlias(requestedAlias);
1081
1110
  const toolkit = canonicalToolkitSlug(slug);
1082
1111
  const mode = connectionMode(cfg);
1083
1112
  const unavailable = managedConnectorUnavailableReason(mode, toolkit);
@@ -1107,8 +1136,15 @@ export async function authorizeService(cfg, slug, requestedAlias) {
1107
1136
  if (usableAccounts.length >= MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit) {
1108
1137
  throw inputError(`${toolkit} already has the maximum of ${MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit} accounts`, 409);
1109
1138
  }
1110
- if (usableAccounts.length > 0 && !alias) {
1111
- throw inputError("Add an account alias so the existing connection is not replaced");
1139
+ if (serviceAccounts.length > 0 && !alias) {
1140
+ if (serviceAccounts.every((account) => /^(initializing|initiated|expired)$/i.test(account.status ?? ""))) {
1141
+ // Retry without replacing a named account or revoking a former grant:
1142
+ // EXPIRED can describe either an abandoned flow or an expired credential.
1143
+ alias = `omb-retry-${randomUUID()}`;
1144
+ }
1145
+ else {
1146
+ throw inputError("Add an account alias so the existing connection is not replaced");
1147
+ }
1112
1148
  }
1113
1149
  if (alias && serviceAccounts.some((account) => account.alias?.trim().toLowerCase() === alias.toLowerCase())) {
1114
1150
  throw inputError(`Account alias "${alias}" is already in use for ${toolkit}`, 409);
@@ -153,18 +153,3 @@ export class ComputerControl {
153
153
  return snapshot;
154
154
  }
155
155
  }
156
- /** Safely executes an async tool or desktop control call, catching runtime exceptions gracefully. */
157
- export async function executeToolSafely(toolName, fn) {
158
- try {
159
- const data = await fn();
160
- return { success: true, data };
161
- }
162
- catch (err) {
163
- const errorMessage = err?.message || String(err) || 'Unknown execution error';
164
- console.error(`[Tool Execution Error] Name: ${toolName} | Details: ${errorMessage}`);
165
- return {
166
- success: false,
167
- error: `Tool '${toolName}' failed to execute: ${errorMessage}`
168
- };
169
- }
170
- }