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,9 @@ const ROSTER_MAX_BOTS = 40;
8
8
  const sectionKey = (section) => section?.trim() || "";
9
9
  /** Dynamic system context for a section's Chief of Staff.
10
10
  * It names the current team on every turn, while list_bots remains the
11
- * authoritative tool for IDs and live availability at delegation time. */
11
+ * authoritative tool for IDs and live availability at delegation time.
12
+ * Who is busy right now is not part of it: that changes turn to turn, so it
13
+ * rides the volatile half (teammateAvailabilityPrompt in peer-roster.ts). */
12
14
  export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoordination = false) {
13
15
  const chief = bots.find((bot) => bot.id === chiefId);
14
16
  const chiefSection = sectionKey(chief?.section);
@@ -17,11 +19,10 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoor
17
19
  // the endpoints must agree, or the prompt names teammates the tools will
18
20
  // then refuse to reach.
19
21
  const team = reachablePeers(bots, chief ?? { id: chiefId, name: "" });
20
- // `about: true` keeps the blurb the Chief staffs from — and keeps this
21
- // prompt byte-identical to what Chiefs have always been given. The
22
- // ordinary-bot roster drops it (peer-roster.ts); widening the Chief's
23
- // existing exposure was never in scope, and narrowing it here would
24
- // silently change how a Chief picks a specialist.
22
+ // `about: true` keeps the blurb the Chief staffs from. The ordinary-bot
23
+ // roster drops it (peer-roster.ts); widening the Chief's existing exposure
24
+ // was never in scope, and narrowing it here would silently change how a
25
+ // Chief picks a specialist.
25
26
  const roster = renderRoster(team, {
26
27
  max: ROSTER_MAX_BOTS,
27
28
  empty: "- No other visible bots are available yet.",
@@ -118,7 +118,7 @@ const NATIVE = [
118
118
  async function connectNative(choice, id, entry, io, deps) {
119
119
  const cli = typeof rawObject(entry.config).cli === "string" ? rawObject(entry.config).cli : choice.cli;
120
120
  io.log("Connecting your account…");
121
- io.log("Sign-in stays with your provider. Its account limits apply; OMB never asks for your password.");
121
+ io.log("Sign-in stays with your provider. Its account limits apply; OpenMausBot never asks for your password.");
122
122
  let state = await deps.inspect(id, entry);
123
123
  if (state.snapshot.state !== "available") {
124
124
  if (cli !== choice.cli)
@@ -143,7 +143,7 @@ async function connectNative(choice, id, entry, io, deps) {
143
143
  await deps.runCli(cli, args, entry.environment);
144
144
  state = await deps.inspect(id, entry);
145
145
  if (state.snapshot.state !== "available" || !state.snapshot.authenticated) {
146
- throw new Error("Sign-in was not confirmed. Your OMB settings are unchanged; complete provider sign-in and try again.");
146
+ throw new Error("Sign-in was not confirmed. Your OpenMausBot settings are unchanged; complete provider sign-in and try again.");
147
147
  }
148
148
  }
149
149
  else {
@@ -294,7 +294,7 @@ export async function runSetup(options, io = defaultSetupIo(), deps = dependenci
294
294
  catch (error) {
295
295
  if (!(error instanceof SetupCancelled))
296
296
  throw error;
297
- io.log("\nSetup cancelled. No OMB settings were changed. Provider sign-ins or installs already completed are kept.");
297
+ io.log("\nSetup cancelled. No OpenMausBot settings were changed. Provider sign-ins or installs already completed are kept.");
298
298
  return false;
299
299
  }
300
300
  finally {
@@ -30,11 +30,13 @@ import { dirname, join, resolve } from "node:path";
30
30
  import { createInterface } from "node:readline/promises";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import qrcode from "qrcode-terminal";
33
+ import { phonePairingLink } from "../shared/pairing-link.js";
33
34
  import { parseAllowList } from "./account-signin.js";
34
35
  import { appendAdminAction, flushAdminActivity, sharedSignIn } from "./admin-activity.js";
35
36
  import { bindDecisionRetention, decisionRetentionDays } from "./decision-log.js";
36
37
  import { hostedWorkspaceConfigured } from "./enterprise.js";
37
38
  import { resolveLoopbackTrust } from "./request-auth.js";
39
+ import { restartPolicy } from "./restart.js";
38
40
  import { writeFileAtomic } from "./atomic.js";
39
41
  import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
40
42
  import { runServiceCommand } from "./service-cli.js";
@@ -466,15 +468,6 @@ export function pairingBlock(input) {
466
468
  }
467
469
  return lines.join("\n");
468
470
  }
469
- /** The scheme and host of a link, or null if it is not one we can dial. */
470
- function originOf(link) {
471
- try {
472
- return new URL(link).origin;
473
- }
474
- catch {
475
- return null;
476
- }
477
- }
478
471
  export function qrToString(text) {
479
472
  let out = "";
480
473
  qrcode.generate(text, { small: true }, (rendered) => {
@@ -494,20 +487,18 @@ async function mintPairing(port, options) {
494
487
  if (status !== 200)
495
488
  throw new Error(`server refused to mint a pairing code: ${typeof body?.error === "string" ? body.error : status}`);
496
489
  const url = options.publicUrl ? `${options.publicUrl}/pair#code=${body.code}` : typeof body.url === "string" ? body.url : null;
497
- // A server too old to mint a credential simply has no invite: the web link
498
- // still works, so an upgrade is never required to pair a browser.
499
- // The address the phone will dial. `--public-url` wins, exactly as it does
500
- // for the web link above: a server behind someone else's proxy often does
501
- // not know its own public name, which is what that flag is for. Gate on the
502
- // credential, never on the server's own invite — a server started without
503
- // OMB_PUBLIC_URL returns a credential and no invite, and gating on the
504
- // invite would throw away a secret the CLI has every part it needs to use.
505
- const address = options.publicUrl ?? (typeof body.url === "string" ? originOf(body.url) : null);
506
- // A server too old to mint a credential simply has no invite: the web link
507
- // still works, so an upgrade is never required to pair a browser.
508
- const invite = typeof body.credential === "string" && address
509
- ? `openmausbot://pair?address=${encodeURIComponent(address)}&token=${encodeURIComponent(body.credential)}${typeof body.serverName === "string" ? `&name=${encodeURIComponent(body.serverName)}` : ""}`
510
- : typeof body.inviteUrl === "string" ? body.inviteUrl : null;
490
+ // The phone-app invite. This command asks over loopback, so the server
491
+ // already built it from its own public address with the same builder; it is
492
+ // printed as it came. `--public-url` wins, exactly as it does for the web
493
+ // link above: a server behind someone else's proxy often does not know its
494
+ // own public name, which is what that flag is for. That case gates on the
495
+ // credential, never on the server's invite — a server started without
496
+ // OMB_PUBLIC_URL returns a credential and no invite. A server too old to
497
+ // mint a credential simply has no invite: the web link still works, so an
498
+ // upgrade is never required to pair a browser.
499
+ const invite = (options.publicUrl && typeof body.credential === "string"
500
+ ? phonePairingLink({ address: options.publicUrl, token: body.credential, name: typeof body.serverName === "string" ? body.serverName : undefined })
501
+ : null) ?? (typeof body.inviteUrl === "string" ? body.inviteUrl : null);
511
502
  return pairingBlock({ code: body.code, url, inviteUrl: invite, expiresAt: body.expiresAt, hint: typeof body.hint === "string" ? body.hint : null, phone: options.phone });
512
503
  }
513
504
  // ── commands ───────────────────────────────────────────────────────────
@@ -853,7 +844,7 @@ async function planTunnel(options, log, recovery) {
853
844
  }
854
845
  }
855
846
  else {
856
- account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery });
847
+ account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery, log: (line) => log(`tunnel: ${line}`) });
857
848
  if (account.credentials.status === "unavailable")
858
849
  return fail(`${account.credentials.file} exists but could not be read; fix or remove it`);
859
850
  if (!describeTunnelAccount(account.credentials.read()).email) {
@@ -1156,9 +1147,25 @@ export async function runServe(options, log = console.log) {
1156
1147
  process.removeListener("SIGTERM", onSignal);
1157
1148
  }
1158
1149
  }
1150
+ /** `serve` until the server stops for good. A server that exits with
1151
+ * RESTART_EXIT_CODE (a copied workspace committed; docs/copy-workspace.md)
1152
+ * is started again in this process, with its tunnel, Tailscale or domain
1153
+ * address unchanged, and without a second pairing code or browser tab; at
1154
+ * most MAX_RESTARTS times in a row (server/restart.ts restartPolicy). */
1155
+ export async function serveUntilStopped(options, run = runServe, now = Date.now) {
1156
+ const policy = restartPolicy(now);
1157
+ let launch = options;
1158
+ for (;;) {
1159
+ const code = await run(launch);
1160
+ if (!policy.again(code))
1161
+ return code;
1162
+ console.log("\nOpenMausBot is starting again to finish installing a copy from the desktop app…");
1163
+ launch = { ...options, pair: false, open: false };
1164
+ }
1165
+ }
1159
1166
  /** Keep setup imports behind the data-dir override: config binds its paths
1160
1167
  * when first imported. `serve` remains usable with stdin closed. */
1161
- export async function runOnboardingCommand(options, io = defaultIo(), startServer = runServe, flow = {}) {
1168
+ export async function runOnboardingCommand(options, io = defaultIo(), startServer = serveUntilStopped, flow = {}) {
1162
1169
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1163
1170
  if (options.command === "setup" && !interactive) {
1164
1171
  io.error("Setup needs an interactive terminal. Run `npx openmausbot setup` in a terminal, then use `npx openmausbot serve` for unattended starts.");
@@ -1235,7 +1242,7 @@ export async function main(argv = process.argv.slice(2)) {
1235
1242
  case "start":
1236
1243
  return runOnboardingCommand(options);
1237
1244
  case "serve":
1238
- return runServe(options);
1245
+ return serveUntilStopped(options);
1239
1246
  case "pair":
1240
1247
  return runPair(options);
1241
1248
  case "sessions":
@@ -15,6 +15,8 @@ export const CLOUD_COMPUTER_TOOLS = [
15
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
16
  tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
17
17
  ];
18
+ /** A call the gate let through while no Boat had landed for the turn. */
19
+ export const CLOUD_COMPUTER_NOT_READY = "The cloud computer is not ready. This call was not performed. Take a fresh screenshot in a moment.";
18
20
  const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
19
21
  const buttons = { left: 1, middle: 2, right: 3 };
20
22
  const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
@@ -85,9 +87,11 @@ export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
85
87
  }
86
88
  /** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
87
89
  * `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. */
90
+ * the control gate (which may claim the seat, and for a cloud computer
91
+ * mounted before it existed, create or wake it), a person's control refuses
92
+ * the call before anything reaches the Boat, the Boat is read only once the
93
+ * gate has let the call through, and the turn's capability must still be
94
+ * live before and after the Boat acts. Listing the tools touches no Boat. */
91
95
  export async function cloudComputerRpc(request, deps) {
92
96
  if (request?.method === "tools/list") {
93
97
  deps.assertActive();
@@ -104,7 +108,10 @@ export async function cloudComputerRpc(request, deps) {
104
108
  deps.assertActive();
105
109
  if (control.held)
106
110
  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);
111
+ const boxId = deps.boxId();
112
+ if (!boxId)
113
+ return { isError: true, content: [{ type: "text", text: CLOUD_COMPUTER_NOT_READY }] };
114
+ const result = await runCloudComputerTool(deps.cfg, boxId, params.name, args, deps.signal);
108
115
  deps.assertActive();
109
116
  return result;
110
117
  }
@@ -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,16 +113,11 @@ 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);
123
119
  if (!config)
124
- throw new Error("This image runs an OMB Cloud home machine; set its boot contract (docs/cloud-pro.md).");
120
+ throw new Error("This image runs a My Cloud machine for OpenMausBot Cloud; set its boot contract (docs/cloud-pro.md).");
125
121
  // Logged here once: the server child never sees what they are about.
126
122
  for (const warning of config.warnings)
127
123
  console.warn(`cloud home: ${warning}`);
@@ -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));
@@ -45,10 +45,6 @@ export const CLOUD_PAIRING_SKEW_S = 300;
45
45
  export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
46
46
  const MAX_NONCES = 10_000;
47
47
  export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
48
- /** The server exits with this after a restore commits (Move to Cloud): the
49
- * launcher then starts it again, and startup installs the restore. Any other
50
- * exit stops the machine for Fly to restart. */
51
- export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
52
48
  export function cloudHomeConfigured(env = process.env) {
53
49
  return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
54
50
  }
@@ -113,17 +109,11 @@ export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
113
109
  * place it offers. A turn's error shows 160 characters, so each fits. */
114
110
  export function cloudHomePlaceRefusal(place) {
115
111
  if (place === "local")
116
- return "This computer isn't a place on your OMB Cloud: its bots run in the cloud. Set Works on to Auto, Cloud or Browser, or lend your Mac under Settings → OMB Cloud.";
112
+ return "This computer isn't a place on My Cloud. Set Works on to Auto, Cloud computer or Browser, or lend your Mac under Settings → OpenMausBot Cloud.";
117
113
  if (place === "vm")
118
- return "Bots on your OMB Cloud can't use a Local VM: the cloud machine has no container runtime. Set Works on to Auto, Cloud or Browser.";
114
+ return "Bots on My Cloud can't use a Local VM. Set Works on to Auto, Cloud computer or Browser.";
119
115
  return undefined;
120
116
  }
121
- /** What a turn is told when Cloud is chosen but no Boat account is set up (no
122
- * key of the person's and no included Boat). A Cloud home has no Local VM to
123
- * suggest instead. */
124
- export function boatNotConfiguredMessage(cloudHome) {
125
- return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
126
- }
127
117
  /** The Cloud's setup checklist (docs/cloud-pro.md) has a "try something"
128
118
  * step that is done once a bot's turn finishes on the machine itself. The
129
119
  * server records when, once, in this Cloud's own onboarding record: that
@@ -202,7 +192,7 @@ export function createCloudPairing(options) {
202
192
  const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, browser ? CLOUD_BROWSER_SIGN_IN_MAX_TTL_S : CLOUD_PAIRING_MAX_TTL_S);
203
193
  const opened = sessions.openPairing({
204
194
  scopes: ["admin", "client"],
205
- label: typeof label === "string" && label.trim() ? label.trim() : "OMB Cloud",
195
+ label: typeof label === "string" && label.trim() ? label.trim() : "OpenMausBot Cloud",
206
196
  ttlMs: ttl * 1000,
207
197
  browser,
208
198
  ...(browser ? { owner: owner } : {}),
@@ -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
@@ -29,7 +29,7 @@
29
29
  import { lstatSync, readFileSync } from "node:fs";
30
30
  import { z } from "zod";
31
31
  import { writeFileAtomic } from "./atomic.js";
32
- export const CLOUD_PERSONAL_REFUSAL = "OMB Cloud is personal: only your own devices can connect.";
32
+ export const CLOUD_PERSONAL_REFUSAL = "My Cloud is personal: only your own devices can connect.";
33
33
  const KEY = /^p_[\w-]{22}$/;
34
34
  const keys = z.array(z.string().regex(KEY)).max(100_000);
35
35
  const recordFile = z.object({
@@ -1106,7 +1106,7 @@ export async function removeAccount(cfg, slug, accountId) {
1106
1106
  }
1107
1107
  /** Mint a browser auth link for one service. Returns { url } or throws. */
1108
1108
  export async function authorizeService(cfg, slug, requestedAlias) {
1109
- const alias = normalizeAccountAlias(requestedAlias);
1109
+ let alias = normalizeAccountAlias(requestedAlias);
1110
1110
  const toolkit = canonicalToolkitSlug(slug);
1111
1111
  const mode = connectionMode(cfg);
1112
1112
  const unavailable = managedConnectorUnavailableReason(mode, toolkit);
@@ -1136,8 +1136,15 @@ export async function authorizeService(cfg, slug, requestedAlias) {
1136
1136
  if (usableAccounts.length >= MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit) {
1137
1137
  throw inputError(`${toolkit} already has the maximum of ${MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit} accounts`, 409);
1138
1138
  }
1139
- if (usableAccounts.length > 0 && !alias) {
1140
- 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
+ }
1141
1148
  }
1142
1149
  if (alias && serviceAccounts.some((account) => account.alias?.trim().toLowerCase() === alias.toLowerCase())) {
1143
1150
  throw inputError(`Account alias "${alias}" is already in use for ${toolkit}`, 409);