openmausbot 0.1.89 → 0.1.91

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 (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -1,11 +1,11 @@
1
- // Box (boat.dev) provider — the bot's cloud computer. Ported from
1
+ // Boat (boat.dev) provider — the bot's cloud computer. Ported from
2
2
  // agentcal-api src/providers/box.js, reshaped per-bot instead of
3
- // per-customer: every bot gets one persistent box (deterministic name),
3
+ // per-customer: every bot gets one persistent boat (deterministic name),
4
4
  // stop pauses billing while the disk survives, and Join always mints a
5
5
  // FRESH desktop URL (stream tokens rotate on every state change — never
6
6
  // persist one).
7
7
  //
8
- // Substrate facts (probed by agentcal 2026-07-24 on a live box):
8
+ // Substrate facts (probed by agentcal 2026-07-24 on a live boat):
9
9
  // - REST only: POST /boxes/{id}/commands runs shell synchronously.
10
10
  // - stop→archived ~5s, resume→idle ~8s; disk persists, tmux does not.
11
11
  // - X11 desktop with Chrome + Ghostty; passwordless sudo; node 24.
@@ -13,12 +13,13 @@
13
13
  import { createHash } from "node:crypto";
14
14
  import { DATA_DIR } from "./config.js";
15
15
  import { loadEnvironmentId } from "./environment.js";
16
- import { adoptResolvedBox, beginBoxCreate, boxCreateRecoverySnapshot, discardBoxCreate, rememberCreatedBox, resolveBoxCreate, retireDeletedBoxCreate, } from "./box-create-idempotency.js";
17
- import { boxDeletionSnapshot, getBoxDeletion, hasPendingBoxDeletionForBot, markBoxDeletionAccepted, markBoxDeletionBlocked, prepareBoxDeletion, retireBoxDeletion, } from "./box-delete-journal.js";
16
+ import { boatCredential, boatProviderApi } from "./included-services.js";
17
+ import { adoptResolvedBoat, beginBoatCreate, boatCreateRecoverySnapshot, discardBoatCreate, rememberCreatedBoat, resolveBoatCreate, retireDeletedBoatCreate, } from "./boat-create-idempotency.js";
18
+ import { boatDeletionSnapshot, getBoatDeletion, hasPendingBoatDeletionForBot, markBoatDeletionAccepted, markBoatDeletionBlocked, prepareBoatDeletion, retireBoatDeletion, } from "./boat-delete-journal.js";
18
19
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
19
20
  export const MAX_REMOTE_COMMAND_LENGTH = 4_000;
20
21
  /** Run an owner-supplied console command without inheriting provider or
21
- * account credentials from the box's environment. */
22
+ * account credentials from the boat's environment. */
22
23
  export function isolatedRemoteCommand(command) {
23
24
  return [
24
25
  "exec env -i",
@@ -34,24 +35,24 @@ export function isolatedRemoteCommand(command) {
34
35
  shellQuote(command),
35
36
  ].join(" ");
36
37
  }
37
- // overridable so tests can point at a stub instead of the live provider
38
- const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
38
+ // Boat's provider surface keeps its historical Box-era names: env OMB_BOX_API,
39
+ // base path /api/box/v1, REST paths /boxes/*, and the box_ token prefix.
39
40
  const READY = new Set(["idle", "ready", "running"]);
40
41
  const SLEEPING = new Set(["archived", "archiving", "stopped", "stopping"]);
41
- const DEFAULT_BOX_TTL_SECONDS = 8 * 60 * 60;
42
- const TRIAL_BOX_TTL_SECONDS = 2 * 60 * 60;
43
- const BOX_CREATE_IN_PROGRESS_RETRY_DELAYS_MS = [250, 750, 1_500];
44
- const BOX_DELETE_OPERATION_POLL_DELAYS_MS = [0, 100, 250, 500, 1_000, 2_000];
45
- const BOX_INVENTORY_PAGE_SIZE = 200;
46
- // Current self-serve accounts top out below 2,000 boxes. Keep the walk
42
+ const DEFAULT_BOAT_TTL_SECONDS = 8 * 60 * 60;
43
+ const TRIAL_BOAT_TTL_SECONDS = 2 * 60 * 60;
44
+ const BOAT_CREATE_IN_PROGRESS_RETRY_DELAYS_MS = [250, 750, 1_500];
45
+ const BOAT_DELETE_OPERATION_POLL_DELAYS_MS = [0, 100, 250, 500, 1_000, 2_000];
46
+ const BOAT_INVENTORY_PAGE_SIZE = 200;
47
+ // Current self-serve accounts top out below 2,000 boats. Keep the walk
47
48
  // bounded anyway: a broken or adversarial cursor must not hold Settings open.
48
- const MAX_BOX_INVENTORY_PAGES = 10;
49
- const LEGACY_MANAGED_BOX_NAME = /^ogb-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
50
- const SCOPED_MANAGED_BOX_NAME = /^ogb-[a-f0-9]{12}-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
51
- const BOX_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
52
- const BOX_DELETE_OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
53
- const BOX_DELETE_OPERATION_STATES = new Set(["pending", "processing", "blocked", "completed"]);
54
- const BOX_STATES = new Set([
49
+ const MAX_BOAT_INVENTORY_PAGES = 10;
50
+ const LEGACY_MANAGED_BOAT_NAME = /^ogb-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
51
+ const SCOPED_MANAGED_BOAT_NAME = /^ogb-[a-f0-9]{12}-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
52
+ const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
53
+ const BOAT_DELETE_OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
54
+ const BOAT_DELETE_OPERATION_STATES = new Set(["pending", "processing", "blocked", "completed"]);
55
+ const BOAT_STATES = new Set([
55
56
  "init",
56
57
  "idle",
57
58
  "ready",
@@ -68,26 +69,26 @@ const BOX_STATES = new Set([
68
69
  "error",
69
70
  ]);
70
71
  // Provider listings are account-wide. Hash the durable local environment id
71
- // into every new name so another OpenMausBot installation using the same Box
72
+ // into every new name so another OpenMausBot installation using the same Boat
72
73
  // account cannot mistake this installation's computers for abandoned ones.
73
74
  // The environment UUID itself never leaves the local data directory.
74
- let scopedBoxPrefixCache = null;
75
+ let scopedBoatPrefixCache = null;
75
76
  /** Resolve only after server startup has migrated the legacy data directory
76
77
  * and acquired its writer lease. A static-import side effect here used to
77
78
  * create the new directory too early and suppress that migration. */
78
- function scopedBoxPrefix() {
79
- if (scopedBoxPrefixCache)
80
- return scopedBoxPrefixCache;
79
+ function scopedBoatPrefix() {
80
+ if (scopedBoatPrefixCache)
81
+ return scopedBoatPrefixCache;
81
82
  const scope = createHash("sha256")
82
83
  .update(loadEnvironmentId(DATA_DIR))
83
84
  .digest("hex")
84
85
  .slice(0, 12);
85
- scopedBoxPrefixCache = `ogb-${scope}-`;
86
- return scopedBoxPrefixCache;
86
+ scopedBoatPrefixCache = `ogb-${scope}-`;
87
+ return scopedBoatPrefixCache;
87
88
  }
88
- /** Decide lifecycle work before a turn mounts Box. Auto may observe and
89
- * attach an already-ready Box, but only explicit Cloud may create or wake. */
90
- export function boxTurnLifecycleAction({ explicitCloud, canMount, state, }) {
89
+ /** Decide lifecycle work before a turn mounts Boat. Auto may observe and
90
+ * attach an already-ready Boat, but only explicit Cloud may create or wake. */
91
+ export function boatTurnLifecycleAction({ explicitCloud, canMount, state, }) {
91
92
  if (!canMount)
92
93
  return "none";
93
94
  if (state && READY.has(state))
@@ -99,60 +100,64 @@ export function boxTurnLifecycleAction({ explicitCloud, canMount, state, }) {
99
100
  /** Keep one provider account for the whole logical operation. Settings may
100
101
  * replace the shared config object after an async request has started; every
101
102
  * follow-up (rename, readiness, cleanup, etc.) must keep using the credential
102
- * that selected or created the Box in the first place. */
103
- function snapshotBoxConfig(cfg) {
103
+ * that selected or created the Boat in the first place. */
104
+ function snapshotBoatConfig(cfg) {
104
105
  return { box: cfg.box ? { token: cfg.box.token } : undefined };
105
106
  }
106
- function boxFetch(cfg, path, opts = {}) {
107
- return fetch(`${BOX_API}${path}`, {
107
+ /** The base URL follows the credential in use (included-services.ts): an own
108
+ * token goes to Boat (OMB_BOX_API points it at a stub in tests), Cloud Pro's
109
+ * included token only to its relay. */
110
+ function boatFetch(cfg, path, opts = {}) {
111
+ const account = boatAccount(cfg);
112
+ return fetch(`${account?.api ?? boatProviderApi()}${path}`, {
108
113
  ...opts,
109
114
  headers: {
110
- authorization: `Bearer ${cfg.box?.token}`,
115
+ authorization: `Bearer ${account?.token}`,
111
116
  "content-type": "application/json",
112
117
  ...opts.headers,
113
118
  },
114
119
  });
115
120
  }
116
- async function boxJson(cfg, path, opts = {}) {
117
- const res = await boxFetch(cfg, path, opts);
121
+ async function boatJson(cfg, path, opts = {}) {
122
+ const res = await boatFetch(cfg, path, opts);
118
123
  const body = await res.json().catch(() => null);
119
124
  return { ok: res.ok && body?.ok !== false, status: res.status, body };
120
125
  }
121
126
  /** Accept only the immutable identity fields needed to follow a delete. Any
122
- * malformed success envelope falls back to a direct Box read instead of
127
+ * malformed success envelope falls back to a direct Boat read instead of
123
128
  * authorizing journal retirement. */
124
- function boxDeletionOperation(body, boxId, expectedOperationId) {
129
+ function boatDeletionOperation(body, boxId, expectedOperationId) {
125
130
  const operation = body?.operation;
126
131
  const id = typeof operation?.id === "string" ? operation.id : "";
127
132
  const status = typeof operation?.status === "string" ? operation.status : "";
128
- if (!BOX_DELETE_OPERATION_ID.test(id)
133
+ if (!BOAT_DELETE_OPERATION_ID.test(id)
129
134
  || (expectedOperationId !== undefined && id !== expectedOperationId)
130
135
  || operation?.kind !== "box"
131
136
  || operation?.targetId !== boxId
132
- || !BOX_DELETE_OPERATION_STATES.has(status))
137
+ || !BOAT_DELETE_OPERATION_STATES.has(status))
133
138
  return null;
134
139
  return { id, kind: "box", targetId: boxId, status: status };
135
140
  }
136
141
  function deletionBlockedError(boxId) {
137
142
  return Object.assign(new Error(`boat.dev accepted deletion of ${boxId}, but the deletion operation is blocked — check boat.dev and retry`), { status: 409 });
138
143
  }
139
- function boxDeleteProvedAbsent(result) {
144
+ function boatDeleteProvedAbsent(result) {
140
145
  return result.status === 404 || result.status === 410;
141
146
  }
142
147
  /** Retire the create receipt before the deletion fence. If that first durable
143
- * write fails, the fence remains and no caller can reuse a Box whose ownership
148
+ * write fails, the fence remains and no caller can reuse a Boat whose ownership
144
149
  * recovery is uncertain. */
145
- function finishRecordedBoxDeletion(boxId) {
146
- retireDeletedBoxCreate(boxId);
147
- forgetBoxId(boxId);
148
- retireBoxDeletion(boxId);
150
+ function finishRecordedBoatDeletion(boxId) {
151
+ retireDeletedBoatCreate(boxId);
152
+ forgetBoatId(boxId);
153
+ retireBoatDeletion(boxId);
149
154
  }
150
155
  /** Reconcile one durable deletion against the exact provider operation/Box.
151
156
  * Account LIST omission is never evidence: it is eventually consistent. */
152
- async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
157
+ async function reconcileRecordedBoatDeletion(cfg, initial, pollDelaysMs = []) {
153
158
  let record = initial;
154
159
  if (record.phase === "accepted" && record.status === "completed") {
155
- finishRecordedBoxDeletion(record.boxId);
160
+ finishRecordedBoatDeletion(record.boxId);
156
161
  return "confirmed";
157
162
  }
158
163
  if (record.phase === "accepted" && record.operationId) {
@@ -161,7 +166,7 @@ async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
161
166
  await new Promise((resolve) => setTimeout(resolve, delayMs));
162
167
  let polled;
163
168
  try {
164
- polled = await boxJson(cfg, `/deletion-operations/${record.operationId}`, {
169
+ polled = await boatJson(cfg, `/deletion-operations/${record.operationId}`, {
165
170
  signal: AbortSignal.timeout(20_000),
166
171
  });
167
172
  }
@@ -170,27 +175,27 @@ async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
170
175
  }
171
176
  if (!polled.ok)
172
177
  break;
173
- const operation = boxDeletionOperation(polled.body, record.boxId, record.operationId);
178
+ const operation = boatDeletionOperation(polled.body, record.boxId, record.operationId);
174
179
  if (!operation)
175
180
  break;
176
181
  if (operation.status === "blocked") {
177
- record = markBoxDeletionBlocked(record.boxId, operation);
182
+ record = markBoatDeletionBlocked(record.boxId, operation);
178
183
  break;
179
184
  }
180
- record = markBoxDeletionAccepted(record.boxId, operation);
185
+ record = markBoatDeletionAccepted(record.boxId, operation);
181
186
  if (operation.status === "completed") {
182
- finishRecordedBoxDeletion(record.boxId);
187
+ finishRecordedBoatDeletion(record.boxId);
183
188
  return "confirmed";
184
189
  }
185
190
  }
186
191
  }
187
192
  // A direct immutable-id 404/410 is the only alternate completion proof.
188
193
  // A live identity keeps the fence even when the operation endpoint is down.
189
- const inspected = await inspectBoxIdentity(cfg, record.boxId);
194
+ const inspected = await inspectBoatIdentity(cfg, record.boxId);
190
195
  if (!inspected.available)
191
196
  return record.phase === "blocked" ? "blocked" : "pending";
192
197
  if (!inspected.identity) {
193
- finishRecordedBoxDeletion(record.boxId);
198
+ finishRecordedBoatDeletion(record.boxId);
194
199
  return "confirmed";
195
200
  }
196
201
  if (inspected.identity.name !== record.name) {
@@ -201,30 +206,30 @@ async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
201
206
  /** Prove that a replacement token can see every durable deletion target
202
207
  * before Settings swaps credentials. Unlike normal reconciliation, a bare
203
208
  * 404 is not completion proof here: it may simply be a different account. */
204
- export async function verifyBoxDeletionCredential(cfg) {
205
- cfg = snapshotBoxConfig(cfg);
206
- for (const initial of boxDeletionSnapshot()) {
209
+ export async function verifyBoatDeletionCredential(cfg) {
210
+ cfg = snapshotBoatConfig(cfg);
211
+ for (const initial of boatDeletionSnapshot()) {
207
212
  let record = initial;
208
213
  let operationAuthorized = false;
209
214
  if (record.phase === "accepted" && record.operationId) {
210
215
  let polled = null;
211
216
  try {
212
- polled = await boxJson(cfg, `/deletion-operations/${record.operationId}`, {
217
+ polled = await boatJson(cfg, `/deletion-operations/${record.operationId}`, {
213
218
  signal: AbortSignal.timeout(20_000),
214
219
  });
215
220
  }
216
221
  catch {
217
- // The exact Box identity below can still prove account continuity.
222
+ // The exact Boat identity below can still prove account continuity.
218
223
  }
219
224
  if (polled?.ok) {
220
- const operation = boxDeletionOperation(polled.body, record.boxId, record.operationId);
225
+ const operation = boatDeletionOperation(polled.body, record.boxId, record.operationId);
221
226
  if (operation) {
222
227
  operationAuthorized = true;
223
228
  record = operation.status === "blocked"
224
- ? markBoxDeletionBlocked(record.boxId, operation)
225
- : markBoxDeletionAccepted(record.boxId, operation);
229
+ ? markBoatDeletionBlocked(record.boxId, operation)
230
+ : markBoatDeletionAccepted(record.boxId, operation);
226
231
  if (operation.status === "completed") {
227
- finishRecordedBoxDeletion(record.boxId);
232
+ finishRecordedBoatDeletion(record.boxId);
228
233
  continue;
229
234
  }
230
235
  }
@@ -232,39 +237,39 @@ export async function verifyBoxDeletionCredential(cfg) {
232
237
  }
233
238
  if (operationAuthorized)
234
239
  continue;
235
- const inspected = await inspectBoxIdentity(cfg, record.boxId);
240
+ const inspected = await inspectBoatIdentity(cfg, record.boxId);
236
241
  if (inspected.available && inspected.identity?.name === record.name)
237
242
  continue;
238
243
  if (!inspected.available) {
239
- throw Object.assign(new Error(`${inspected.problem ?? "a deleting cloud computer could not be verified"}. Retry with the Box account that owns it`), { status: 503 });
244
+ throw Object.assign(new Error(`${inspected.problem ?? "a deleting cloud computer could not be verified"}. Retry with the Boat account that owns it`), { status: 503 });
240
245
  }
241
- throw Object.assign(new Error("that Box token cannot access the cloud computers whose deletion is still being reconciled"), { status: 409 });
246
+ throw Object.assign(new Error("that Boat token cannot access the cloud computers whose deletion is still being reconciled"), { status: 409 });
242
247
  }
243
248
  }
244
249
  /** Bind a successful DELETE response to the durable target before polling.
245
250
  * A malformed receipt leaves the prepared fence intact. */
246
- async function confirmAcceptedBoxDeletion(cfg, record, acceptedBody, pollDelaysMs = BOX_DELETE_OPERATION_POLL_DELAYS_MS) {
247
- const operation = boxDeletionOperation(acceptedBody, record.boxId);
251
+ async function confirmAcceptedBoatDeletion(cfg, record, acceptedBody, pollDelaysMs = BOAT_DELETE_OPERATION_POLL_DELAYS_MS) {
252
+ const operation = boatDeletionOperation(acceptedBody, record.boxId);
248
253
  if (!operation) {
249
- const inspected = await inspectBoxIdentity(cfg, record.boxId);
254
+ const inspected = await inspectBoatIdentity(cfg, record.boxId);
250
255
  if (inspected.available && !inspected.identity) {
251
- finishRecordedBoxDeletion(record.boxId);
256
+ finishRecordedBoatDeletion(record.boxId);
252
257
  return "confirmed";
253
258
  }
254
259
  throw Object.assign(new Error(`boat.dev returned an invalid deletion receipt for ${record.boxId}; its deletion fence was kept`), { status: 503 });
255
260
  }
256
261
  const next = operation.status === "blocked"
257
- ? markBoxDeletionBlocked(record.boxId, operation)
258
- : markBoxDeletionAccepted(record.boxId, operation);
259
- return reconcileRecordedBoxDeletion(cfg, next, pollDelaysMs);
262
+ ? markBoatDeletionBlocked(record.boxId, operation)
263
+ : markBoatDeletionAccepted(record.boxId, operation);
264
+ return reconcileRecordedBoatDeletion(cfg, next, pollDelaysMs);
260
265
  }
261
266
  /** Send (or explicitly retry) DELETE only after the immutable target is on
262
267
  * disk. The returned pending state always has a validated operation receipt. */
263
- async function requestRecordedBoxDeletion(cfg, identity, pollDelaysMs = BOX_DELETE_OPERATION_POLL_DELAYS_MS) {
264
- const deletion = prepareBoxDeletion(identity);
268
+ async function requestRecordedBoatDeletion(cfg, identity, pollDelaysMs = BOAT_DELETE_OPERATION_POLL_DELAYS_MS) {
269
+ const deletion = prepareBoatDeletion(identity);
265
270
  let removed;
266
271
  try {
267
- removed = await boxJson(cfg, `/boxes/${identity.boxId}`, {
272
+ removed = await boatJson(cfg, `/boxes/${identity.boxId}`, {
268
273
  method: "DELETE",
269
274
  headers: { "X-Ascii-Confirm-Delete": identity.boxId },
270
275
  });
@@ -272,42 +277,42 @@ async function requestRecordedBoxDeletion(cfg, identity, pollDelaysMs = BOX_DELE
272
277
  catch (error) {
273
278
  throw Object.assign(new Error("Could not confirm whether boat.dev accepted the delete. The computer was kept fenced; retry Delete to reconcile it"), { status: 503, cause: error });
274
279
  }
275
- if (boxDeleteProvedAbsent(removed)) {
276
- finishRecordedBoxDeletion(identity.boxId);
280
+ if (boatDeleteProvedAbsent(removed)) {
281
+ finishRecordedBoatDeletion(identity.boxId);
277
282
  return "confirmed";
278
283
  }
279
284
  if (!removed.ok) {
280
- markBoxDeletionBlocked(identity.boxId);
281
- throw Object.assign(new Error(boxErrorMessage(removed.status, "box delete", removed.body)), { status: removed.status });
285
+ markBoatDeletionBlocked(identity.boxId);
286
+ throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body, usesIncludedBoat(cfg))), { status: removed.status });
282
287
  }
283
- const confirmation = await confirmAcceptedBoxDeletion(cfg, deletion, removed.body, pollDelaysMs);
288
+ const confirmation = await confirmAcceptedBoatDeletion(cfg, deletion, removed.body, pollDelaysMs);
284
289
  if (confirmation === "blocked")
285
290
  throw deletionBlockedError(identity.boxId);
286
291
  return confirmation;
287
292
  }
288
- function boxBotNameParts(botId) {
293
+ function boatBotNameParts(botId) {
289
294
  const prefix = botId.slice(0, 8).toLowerCase().replace(/[^a-z0-9]/g, "") || "bot";
290
295
  const hash = createHash("sha256").update(botId).digest("hex").slice(0, 6);
291
296
  return { prefix, hash };
292
297
  }
293
- function legacyBoxNameFor(botId) {
294
- const { prefix, hash } = boxBotNameParts(botId);
298
+ function legacyBoatNameFor(botId) {
299
+ const { prefix, hash } = boatBotNameParts(botId);
295
300
  return `ogb-${prefix}-${hash}`;
296
301
  }
297
302
  // Deterministic per installation and bot. The bot hash kills truncated-id
298
303
  // collisions; the environment scope prevents cross-install ownership claims.
299
- export async function boxNameFor(botId) {
300
- const { prefix, hash } = boxBotNameParts(botId);
301
- return `${scopedBoxPrefix()}${prefix}-${hash}`;
304
+ export async function boatNameFor(botId) {
305
+ const { prefix, hash } = boatBotNameParts(botId);
306
+ return `${scopedBoatPrefix()}${prefix}-${hash}`;
302
307
  }
303
308
  /** Credential restoration must accept both current installation-scoped names
304
309
  * and durable pre-scope names that the ownership journal may have adopted. */
305
- export async function boxNameMatchesBot(botId, name) {
306
- return name === await boxNameFor(botId) || name === legacyBoxNameFor(botId);
310
+ export async function boatNameMatchesBot(botId, name) {
311
+ return name === await boatNameFor(botId) || name === legacyBoatNameFor(botId);
307
312
  }
308
313
  export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000, signal } = {}) {
309
- assertBoxNotDeleting(boxId);
310
- const res = await boxFetch(cfg, `/boxes/${boxId}/commands`, {
314
+ assertBoatNotDeleting(boxId);
315
+ const res = await boatFetch(cfg, `/boxes/${boxId}/commands`, {
311
316
  method: "POST",
312
317
  body: JSON.stringify({ command }),
313
318
  signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
@@ -324,13 +329,13 @@ export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000, sig
324
329
  // 1) VNC (POST /desktop?vnc=1) — plain WebSocket, survives P2P-blocking
325
330
  // networks; answers {provisioning:true} first, so poll for the URL.
326
331
  // 2) WebRTC stream (POST /desktop) as fallback — STUN-only, can hang.
327
- // The desktopUrl stored on the box object is NOT usable on its own.
332
+ // The desktopUrl stored on the boat object is NOT usable on its own.
328
333
  async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
329
- assertBoxNotDeleting(boxId);
334
+ assertBoatNotDeleting(boxId);
330
335
  const t0 = Date.now();
331
336
  while (Date.now() - t0 < vncBudgetMs) {
332
- assertBoxNotDeleting(boxId);
333
- const { body } = await boxJson(cfg, `/boxes/${boxId}/desktop?vnc=1`, { method: "POST" });
337
+ assertBoatNotDeleting(boxId);
338
+ const { body } = await boatJson(cfg, `/boxes/${boxId}/desktop?vnc=1`, { method: "POST" });
334
339
  const url = body?.desktopUrl ?? body?.url;
335
340
  if (url)
336
341
  return url;
@@ -338,68 +343,88 @@ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
338
343
  break;
339
344
  await new Promise((r) => setTimeout(r, 3000));
340
345
  }
341
- const { body } = await boxJson(cfg, `/boxes/${boxId}/desktop`, { method: "POST" });
346
+ const { body } = await boatJson(cfg, `/boxes/${boxId}/desktop`, { method: "POST" });
342
347
  return body?.desktopUrl ?? body?.url ?? null;
343
348
  }
344
349
  async function waitReady(cfg, boxId, budgetMs = 90_000) {
345
- assertBoxNotDeleting(boxId);
350
+ assertBoatNotDeleting(boxId);
346
351
  const t0 = Date.now();
352
+ // Boat's words for the last failed resume, if the wait runs out on them.
353
+ let resumeFailure = null;
347
354
  while (Date.now() - t0 < budgetMs) {
348
- assertBoxNotDeleting(boxId);
349
- const { body } = await boxJson(cfg, `/boxes/${boxId}`);
355
+ assertBoatNotDeleting(boxId);
356
+ const { body } = await boatJson(cfg, `/boxes/${boxId}`);
350
357
  const state = body?.box?.state;
351
358
  if (READY.has(state))
352
359
  return body.box;
353
360
  if (state === "error")
354
361
  return null;
355
- // an archiving box can't resume until the snapshot lands — nudge after
356
- if (state === "archived")
357
- await boxJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
362
+ // an archiving boat can't resume until the snapshot lands — nudge after.
363
+ // A refusal (a plan limit, say) is final: report it now. A server error
364
+ // is retried on the next poll, as Boat asks; 409 is a state race with a
365
+ // wake already under way.
366
+ if (state === "archived") {
367
+ const resumed = await boatJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
368
+ if (resumed.ok)
369
+ resumeFailure = null;
370
+ else if (resumed.status !== 409) {
371
+ const message = boatErrorMessage(resumed.status, "waking the cloud computer", resumed.body, usesIncludedBoat(cfg));
372
+ if (resumed.status < 500)
373
+ throw new Error(message);
374
+ resumeFailure = message;
375
+ }
376
+ }
358
377
  await new Promise((r) => setTimeout(r, 2500));
359
378
  }
379
+ if (resumeFailure)
380
+ throw new Error(resumeFailure);
360
381
  return null;
361
382
  }
362
- // Resolving a bot's box means LISTing every box in the account, so it is
383
+ // Resolving a bot's boat means LISTing every boat in the account, so it is
363
384
  // the most expensive thing on any hot path. The name is deterministic, so
364
385
  // once we know the id we can go straight at it — the cache is refreshed
365
- // whenever the direct read fails (deleted/renamed box) and always carries
386
+ // whenever the direct read fails (deleted/renamed boat) and always carries
366
387
  // the live state so callers can still see "archived".
367
- const boxIdCache = new Map();
368
- function boxInventoryProblem(status, body) {
388
+ const boatIdCache = new Map();
389
+ function boatInventoryProblem(status, body, included = false) {
369
390
  if (status === 401 || status === 403) {
370
- return "boat.dev rejected the Box API key — update it in Settings → Connections";
391
+ return included ? INCLUDED_BOAT_UNAVAILABLE : "boat.dev rejected the Boat API key — update it in Settings → Connections";
371
392
  }
372
393
  if (status === 429)
373
394
  return "boat.dev is rate-limiting this account — wait a minute and refresh";
374
395
  const message = typeof body?.message === "string" ? body.message.trim() : "";
375
396
  return message ? `boat.dev could not list cloud computers: ${message}` : `boat.dev could not list cloud computers (${status})`;
376
397
  }
377
- function safeBoxState(value) {
398
+ function safeBoatState(value) {
378
399
  if (typeof value !== "string")
379
400
  return "unknown";
380
401
  const state = value.toLowerCase();
381
- return BOX_STATES.has(state) ? state : "unknown";
402
+ return BOAT_STATES.has(state) ? state : "unknown";
382
403
  }
383
- async function listBoxPages(cfg) {
384
- const boxes = [];
404
+ async function listBoatPages(cfg) {
405
+ const boats = [];
385
406
  const seenCursors = new Set();
386
407
  let cursor = null;
387
- for (let page = 0; page < MAX_BOX_INVENTORY_PAGES; page += 1) {
388
- const path = `/boxes?limit=${BOX_INVENTORY_PAGE_SIZE}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
408
+ for (let page = 0; page < MAX_BOAT_INVENTORY_PAGES; page += 1) {
409
+ const path = `/boxes?limit=${BOAT_INVENTORY_PAGE_SIZE}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
389
410
  let listed;
390
411
  try {
391
- listed = await boxJson(cfg, path, { signal: AbortSignal.timeout(20_000) });
412
+ listed = await boatJson(cfg, path, { signal: AbortSignal.timeout(20_000) });
392
413
  }
393
414
  catch {
394
415
  return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
395
416
  }
396
417
  if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
397
- return { ok: false, problem: boxInventoryProblem(listed.status, listed.body), credentialRejected: listed.status === 401 || listed.status === 403 };
418
+ return {
419
+ ok: false,
420
+ problem: boatInventoryProblem(listed.status, listed.body, usesIncludedBoat(cfg)),
421
+ credentialRejected: listed.status === 401 || listed.status === 403,
422
+ };
398
423
  }
399
- boxes.push(...listed.body.boxes);
424
+ boats.push(...listed.body.boxes);
400
425
  const next = listed.body?.pageInfo?.nextCursor;
401
426
  if (next === undefined || next === null || next === "")
402
- return { ok: true, boxes };
427
+ return { ok: true, boats };
403
428
  if (typeof next !== "string" || next.length > 4_096) {
404
429
  return { ok: false, problem: "boat.dev returned an invalid cloud computer page cursor" };
405
430
  }
@@ -412,7 +437,7 @@ async function listBoxPages(cfg) {
412
437
  return { ok: false, problem: "boat.dev returned too many cloud computer pages — narrow the account inventory and refresh" };
413
438
  }
414
439
  /**
415
- * One account listing for Settings and deletion guards. Only boxes
440
+ * One account listing for Settings and deletion guards. Only boats
416
441
  * carrying OpenMausBot's exact deterministic name shape leave this boundary;
417
442
  * provider desktop links, IPs, environment details and other raw fields never
418
443
  * reach the renderer. Only names scoped to this installation may become
@@ -420,12 +445,12 @@ async function listBoxPages(cfg) {
420
445
  * ownership; foreign-install and ownerless legacy rows remain invisible and
421
446
  * therefore cannot become deletion targets.
422
447
  */
423
- export async function listManagedBoxes(cfg, owners, options = {}) {
424
- cfg = snapshotBoxConfig(cfg);
425
- if (!boxConfigured(cfg)) {
448
+ export async function listManagedBoats(cfg, owners, options = {}) {
449
+ cfg = snapshotBoatConfig(cfg);
450
+ if (!boatConfigured(cfg)) {
426
451
  return { configured: false, available: false, problem: null, instances: [] };
427
452
  }
428
- const listed = await listBoxPages(cfg);
453
+ const listed = await listBoatPages(cfg);
429
454
  if (!listed.ok) {
430
455
  return {
431
456
  configured: true,
@@ -436,8 +461,8 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
436
461
  };
437
462
  }
438
463
  const namedOwners = await Promise.all(owners.map(async (owner) => ({
439
- currentName: await boxNameFor(owner.botId),
440
- legacyName: legacyBoxNameFor(owner.botId),
464
+ currentName: await boatNameFor(owner.botId),
465
+ legacyName: legacyBoatNameFor(owner.botId),
441
466
  owner,
442
467
  })));
443
468
  const invalidInventory = (problem) => ({
@@ -451,12 +476,12 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
451
476
  // authoritative direct endpoint so Settings can still display and delete
452
477
  // the computer. Credential replacement probes deliberately opt out: their
453
478
  // token must be judged only by the account inventory it can list.
454
- let candidates = [...listed.boxes];
479
+ let candidates = [...listed.boats];
455
480
  if (options.adoptLegacy !== false) {
456
481
  const namedOwnerByBotId = new Map(namedOwners.map((entry) => [entry.owner.botId, entry]));
457
482
  let recoveries;
458
483
  try {
459
- recoveries = boxCreateRecoverySnapshot();
484
+ recoveries = boatCreateRecoverySnapshot();
460
485
  }
461
486
  catch {
462
487
  return invalidInventory("OpenMausBot could not safely read its cloud computer recovery records");
@@ -478,14 +503,14 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
478
503
  }
479
504
  continue;
480
505
  }
481
- const inspected = await inspectBoxIdentity(cfg, recovery.boxId);
506
+ const inspected = await inspectBoatIdentity(cfg, recovery.boxId);
482
507
  if (!inspected.available) {
483
508
  return invalidInventory(inspected.problem ?? "A remembered cloud computer could not be verified");
484
509
  }
485
510
  if (!inspected.identity) {
486
511
  // Direct 404/410 is stronger than an eventually-consistent LIST row.
487
512
  candidates = candidates.filter((candidate) => candidate?.id !== recovery.boxId);
488
- retireDeletedBoxCreate(recovery.boxId);
513
+ retireDeletedBoatCreate(recovery.boxId);
489
514
  continue;
490
515
  }
491
516
  if (inspected.identity.name !== namedOwner.currentName
@@ -501,7 +526,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
501
526
  }
502
527
  let deletions;
503
528
  try {
504
- deletions = boxDeletionSnapshot();
529
+ deletions = boatDeletionSnapshot();
505
530
  }
506
531
  catch {
507
532
  return invalidInventory("OpenMausBot could not safely read its cloud computer deletion records");
@@ -509,7 +534,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
509
534
  for (const deletion of deletions) {
510
535
  let state;
511
536
  try {
512
- state = await reconcileRecordedBoxDeletion(cfg, deletion, [0]);
537
+ state = await reconcileRecordedBoatDeletion(cfg, deletion, [0]);
513
538
  }
514
539
  catch (error) {
515
540
  return invalidInventory(error instanceof Error ? error.message : "A cloud computer deletion could not be verified");
@@ -528,13 +553,13 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
528
553
  if (matchingRows[0]?.name !== deletion.name) {
529
554
  return invalidInventory("A cloud computer being deleted no longer has its remembered name — repair it in boat.dev before continuing");
530
555
  }
531
- if (getBoxDeletion(deletion.boxId)?.phase === "accepted") {
556
+ if (getBoatDeletion(deletion.boxId)?.phase === "accepted") {
532
557
  matchingRows[0] = { ...matchingRows[0], state: "removing" };
533
558
  candidates = candidates.map((candidate) => candidate?.id === deletion.boxId ? matchingRows[0] : candidate);
534
559
  }
535
560
  continue;
536
561
  }
537
- const current = getBoxDeletion(deletion.boxId);
562
+ const current = getBoatDeletion(deletion.boxId);
538
563
  if (!current)
539
564
  continue;
540
565
  if (current.phase === "accepted") {
@@ -543,7 +568,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
543
568
  }
544
569
  // A prepared request may have lost its response, and a blocked request
545
570
  // is retryable. Keep the exact row actionable only after a direct read.
546
- const inspected = await inspectBoxIdentity(cfg, current.boxId);
571
+ const inspected = await inspectBoatIdentity(cfg, current.boxId);
547
572
  if (!inspected.available || !inspected.identity || inspected.identity.name !== current.name) {
548
573
  return invalidInventory(inspected.problem ?? "A cloud computer deletion target could not be verified");
549
574
  }
@@ -556,15 +581,15 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
556
581
  }
557
582
  const ownerByCurrentName = new Map(namedOwners.map(({ currentName, owner }) => [currentName, owner]));
558
583
  const ownerByLegacyName = new Map(namedOwners.map(({ legacyName, owner }) => [legacyName, owner]));
559
- const boxIdCounts = new Map();
584
+ const boatIdCounts = new Map();
560
585
  for (const candidate of candidates) {
561
586
  if (!candidate || typeof candidate !== "object")
562
587
  continue;
563
588
  const boxId = typeof candidate.id === "string" ? candidate.id : "";
564
- if (BOX_ID.test(boxId))
565
- boxIdCounts.set(boxId, (boxIdCounts.get(boxId) ?? 0) + 1);
589
+ if (BOAT_ID.test(boxId))
590
+ boatIdCounts.set(boxId, (boatIdCounts.get(boxId) ?? 0) + 1);
566
591
  }
567
- const ownedBoxByBot = new Map();
592
+ const ownedBoatByBot = new Map();
568
593
  for (const candidate of candidates) {
569
594
  if (!candidate || typeof candidate !== "object")
570
595
  continue;
@@ -573,18 +598,18 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
573
598
  if (!owner)
574
599
  continue;
575
600
  const boxId = typeof candidate.id === "string" ? candidate.id : "";
576
- if (!BOX_ID.test(boxId)) {
601
+ if (!BOAT_ID.test(boxId)) {
577
602
  return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
578
603
  }
579
- const existing = ownedBoxByBot.get(owner.botId);
604
+ const existing = ownedBoatByBot.get(owner.botId);
580
605
  if (existing && existing !== boxId) {
581
606
  return invalidInventory("boat.dev returned conflicting cloud computers for one OpenMaus bot — repair them in boat.dev before continuing");
582
607
  }
583
- ownedBoxByBot.set(owner.botId, boxId);
608
+ ownedBoatByBot.set(owner.botId, boxId);
584
609
  }
585
610
  const instances = [];
586
- const seenBoxIds = new Set();
587
- const scopedPrefix = scopedBoxPrefix();
611
+ const seenBoatIds = new Set();
612
+ const scopedPrefix = scopedBoatPrefix();
588
613
  for (const candidate of candidates) {
589
614
  if (!candidate || typeof candidate !== "object")
590
615
  continue;
@@ -592,14 +617,14 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
592
617
  const name = typeof candidate.name === "string" ? candidate.name : "";
593
618
  let owner = null;
594
619
  let legacyOwner = false;
595
- if (SCOPED_MANAGED_BOX_NAME.test(name)) {
620
+ if (SCOPED_MANAGED_BOAT_NAME.test(name)) {
596
621
  // A valid OMB name for another environment is account-visible but not
597
622
  // ours to display or mutate.
598
623
  if (!name.startsWith(scopedPrefix))
599
624
  continue;
600
625
  owner = ownerByCurrentName.get(name) ?? null;
601
626
  }
602
- else if (LEGACY_MANAGED_BOX_NAME.test(name)) {
627
+ else if (LEGACY_MANAGED_BOAT_NAME.test(name)) {
603
628
  // Pre-scope names have no installation provenance. A live local bot is
604
629
  // the only safe ownership proof; unmatched legacy rows stay provider-
605
630
  // managed until the person handles them in boat.dev directly.
@@ -614,25 +639,25 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
614
639
  // Once a row names this installation (or a live bot through its legacy
615
640
  // deterministic name), silently skipping a malformed/duplicated identity
616
641
  // could let bot deletion mistake provider corruption for absence.
617
- if (!BOX_ID.test(boxId)) {
642
+ if (!BOAT_ID.test(boxId)) {
618
643
  return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
619
644
  }
620
- if ((boxIdCounts.get(boxId) ?? 0) !== 1 || seenBoxIds.has(boxId)) {
645
+ if ((boatIdCounts.get(boxId) ?? 0) !== 1 || seenBoatIds.has(boxId)) {
621
646
  return invalidInventory("boat.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
622
647
  }
623
648
  if (legacyOwner && owner && options.adoptLegacy !== false) {
624
649
  try {
625
- adoptResolvedBox(owner.botId, boxId);
650
+ adoptResolvedBoat(owner.botId, boxId);
626
651
  }
627
652
  catch {
628
653
  return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in boat.dev before continuing");
629
654
  }
630
655
  }
631
- seenBoxIds.add(boxId);
656
+ seenBoatIds.add(boxId);
632
657
  instances.push({
633
658
  boxId,
634
659
  name,
635
- state: safeBoxState(candidate.state),
660
+ state: safeBoatState(candidate.state),
636
661
  ownerBotId: owner?.botId ?? null,
637
662
  ownerName: owner?.name ?? null,
638
663
  orphaned: owner === null,
@@ -646,18 +671,18 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
646
671
  });
647
672
  return { configured: true, available: true, problem: null, instances };
648
673
  }
649
- /** Direct identity proof for a Box remembered in the local create journal.
674
+ /** Direct identity proof for a Boat remembered in the local create journal.
650
675
  * Unlike account LIST, this endpoint is not eventually consistent. Only the
651
676
  * immutable id, provider name and allowlisted lifecycle state cross this
652
677
  * boundary. */
653
- export async function inspectBoxIdentity(cfg, boxId) {
654
- cfg = snapshotBoxConfig(cfg);
655
- if (!BOX_ID.test(boxId)) {
678
+ export async function inspectBoatIdentity(cfg, boxId) {
679
+ cfg = snapshotBoatConfig(cfg);
680
+ if (!BOAT_ID.test(boxId)) {
656
681
  return { available: false, identity: null, problem: "the remembered cloud computer id is invalid" };
657
682
  }
658
683
  let inspected;
659
684
  try {
660
- inspected = await boxJson(cfg, `/boxes/${boxId}`, { signal: AbortSignal.timeout(20_000) });
685
+ inspected = await boatJson(cfg, `/boxes/${boxId}`, { signal: AbortSignal.timeout(20_000) });
661
686
  }
662
687
  catch {
663
688
  return {
@@ -670,7 +695,7 @@ export async function inspectBoxIdentity(cfg, boxId) {
670
695
  return { available: true, identity: null, problem: null };
671
696
  }
672
697
  if (!inspected.ok) {
673
- return { available: false, identity: null, problem: boxInventoryProblem(inspected.status, inspected.body) };
698
+ return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body, usesIncludedBoat(cfg)) };
674
699
  }
675
700
  const candidate = inspected.body?.box;
676
701
  const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
@@ -678,30 +703,30 @@ export async function inspectBoxIdentity(cfg, boxId) {
678
703
  if (returnedId !== boxId || name.length === 0 || name.length > 100 || /[\r\n]/.test(name)) {
679
704
  return { available: false, identity: null, problem: "boat.dev returned an invalid cloud computer identity" };
680
705
  }
681
- return { available: true, identity: { boxId, name, state: safeBoxState(candidate.state) }, problem: null };
706
+ return { available: true, identity: { boxId, name, state: safeBoatState(candidate.state) }, problem: null };
682
707
  }
683
708
  function inventoryFailure(inventory) {
684
709
  const error = new Error(inventory.configured
685
710
  ? (inventory.problem ?? "Cloud computer inventory is unavailable")
686
- : "Box is not configured — add its API key in Settings → Connections");
711
+ : "Boat is not configured — add its API key in Settings → Connections");
687
712
  error.status = inventory.configured ? 503 : 409;
688
713
  return error;
689
714
  }
690
715
  function deletionFenceError() {
691
716
  return Object.assign(new Error("this cloud computer is being deleted — wait for it to finish, or retry Delete if it needs attention"), { status: 409 });
692
717
  }
693
- function assertBoxNotDeleting(boxId) {
694
- if (getBoxDeletion(boxId))
718
+ function assertBoatNotDeleting(boxId) {
719
+ if (getBoatDeletion(boxId))
695
720
  throw deletionFenceError();
696
721
  }
697
- function assertBotBoxNotDeleting(botId) {
698
- if (hasPendingBoxDeletionForBot(botId))
722
+ function assertBotBoatNotDeleting(botId) {
723
+ if (hasPendingBoatDeletionForBot(botId))
699
724
  throw deletionFenceError();
700
725
  }
701
- async function revalidateManagedBox(cfg, owners, boxId) {
702
- if (!BOX_ID.test(boxId))
726
+ async function revalidateManagedBoat(cfg, owners, boxId) {
727
+ if (!BOAT_ID.test(boxId))
703
728
  throw Object.assign(new Error("invalid cloud computer id"), { status: 400 });
704
- const inventory = await listManagedBoxes(cfg, owners);
729
+ const inventory = await listManagedBoats(cfg, owners);
705
730
  if (!inventory.available)
706
731
  throw inventoryFailure(inventory);
707
732
  const instance = inventory.instances.find((candidate) => candidate.boxId === boxId);
@@ -714,27 +739,28 @@ const QUIESCE_BROWSER = [
714
739
  'for name in chrome google-chrome chromium chromium-browser; do pid=$(pgrep -o -x "$name" 2>/dev/null || true); [ -z "$pid" ] || kill -TERM "$pid" 2>/dev/null || true; done',
715
740
  'for i in 1 2 3 4 5 6 7 8; do if ! pgrep -x chrome >/dev/null 2>&1 && ! pgrep -x google-chrome >/dev/null 2>&1 && ! pgrep -x chromium >/dev/null 2>&1 && ! pgrep -x chromium-browser >/dev/null 2>&1; then break; fi; sleep 0.25; done',
716
741
  ].join("; ");
717
- async function stopBox(cfg, boxId) {
718
- assertBoxNotDeleting(boxId);
742
+ async function stopBoat(cfg, boxId) {
743
+ assertBoatNotDeleting(boxId);
719
744
  // Browser shutdown is best-effort, but the provider stop is not: Settings
720
745
  // must never say a computer is sleeping when boat.dev rejected the action.
721
746
  await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
722
- const stopped = await boxJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
723
- if (!stopped.ok)
724
- throw Object.assign(new Error(boxErrorMessage(stopped.status, "box sleep", stopped.body)), { status: stopped.status });
747
+ const stopped = await boatJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
748
+ if (!stopped.ok) {
749
+ throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body, usesIncludedBoat(cfg))), { status: stopped.status });
750
+ }
725
751
  }
726
- function forgetBoxId(boxId) {
727
- for (const [botId, cachedId] of boxIdCache) {
752
+ function forgetBoatId(boxId) {
753
+ for (const [botId, cachedId] of boatIdCache) {
728
754
  if (cachedId === boxId)
729
- boxIdCache.delete(botId);
755
+ boatIdCache.delete(botId);
730
756
  }
731
757
  }
732
758
  /** Explicit Settings action. Re-listing prevents a stale row from targeting a
733
- * renamed or foreign provider resource. This never wakes or joins a Box. */
734
- export async function sleepManagedBox(cfg, owners, boxId, claim) {
735
- cfg = snapshotBoxConfig(cfg);
736
- assertBoxNotDeleting(boxId);
737
- const instance = await revalidateManagedBox(cfg, owners, boxId);
759
+ * renamed or foreign provider resource. This never wakes or joins a Boat. */
760
+ export async function sleepManagedBoat(cfg, owners, boxId, claim) {
761
+ cfg = snapshotBoatConfig(cfg);
762
+ assertBoatNotDeleting(boxId);
763
+ const instance = await revalidateManagedBoat(cfg, owners, boxId);
738
764
  if (instance.inUse) {
739
765
  throw Object.assign(new Error("this cloud computer is in use — stop its bot's work first"), { status: 409 });
740
766
  }
@@ -744,8 +770,8 @@ export async function sleepManagedBox(cfg, owners, boxId, claim) {
744
770
  const release = claim?.(instance);
745
771
  try {
746
772
  if (!SLEEPING.has(instance.state))
747
- await stopBox(cfg, instance.boxId);
748
- forgetBoxId(instance.boxId);
773
+ await stopBoat(cfg, instance.boxId);
774
+ forgetBoatId(instance.boxId);
749
775
  return { ok: true };
750
776
  }
751
777
  finally {
@@ -755,23 +781,23 @@ export async function sleepManagedBox(cfg, owners, boxId, claim) {
755
781
  /** Permanent Settings action. The caller must echo the exact freshly-listed
756
782
  * machine name as well as its id; boat.dev independently requires the id in
757
783
  * its confirmation header. */
758
- export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim, options = {}) {
759
- cfg = snapshotBoxConfig(cfg);
760
- const remembered = getBoxDeletion(boxId);
784
+ export async function deleteManagedBoat(cfg, owners, boxId, confirmName, claim, options = {}) {
785
+ cfg = snapshotBoatConfig(cfg);
786
+ const remembered = getBoatDeletion(boxId);
761
787
  if (remembered) {
762
- const reconciled = await reconcileRecordedBoxDeletion(cfg, remembered, [0]);
788
+ const reconciled = await reconcileRecordedBoatDeletion(cfg, remembered, [0]);
763
789
  if (reconciled === "confirmed")
764
790
  return { ok: true };
765
791
  // A validated accepted operation owns this target. Retrying DELETE would
766
792
  // create a second operation and weaken the only trustworthy receipt.
767
- const current = getBoxDeletion(boxId);
793
+ const current = getBoatDeletion(boxId);
768
794
  if (current?.phase === "accepted") {
769
795
  return { ok: true, pending: true };
770
796
  }
771
797
  // Prepared (ambiguous request) and blocked records may be retried only by
772
798
  // this explicit Settings/bot-deletion path after fresh identity checks.
773
799
  }
774
- const instance = await revalidateManagedBox(cfg, owners, boxId);
800
+ const instance = await revalidateManagedBoat(cfg, owners, boxId);
775
801
  if (instance.inUse) {
776
802
  throw Object.assign(new Error("this cloud computer is in use — stop its bot's work first"), { status: 409 });
777
803
  }
@@ -780,13 +806,13 @@ export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim, o
780
806
  }
781
807
  const release = claim?.(instance);
782
808
  try {
783
- const confirmation = await requestRecordedBoxDeletion(cfg, {
809
+ const confirmation = await requestRecordedBoatDeletion(cfg, {
784
810
  boxId: instance.boxId,
785
811
  name: instance.name,
786
812
  ownerBotId: instance.ownerBotId,
787
813
  }, options.pollDelaysMs);
788
814
  if (confirmation === "pending") {
789
- forgetBoxId(instance.boxId);
815
+ forgetBoatId(instance.boxId);
790
816
  return { ok: true, pending: true };
791
817
  }
792
818
  return { ok: true };
@@ -795,72 +821,87 @@ export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim, o
795
821
  release?.();
796
822
  }
797
823
  }
798
- export async function findBox(cfg, botId) {
799
- cfg = snapshotBoxConfig(cfg);
800
- assertBotBoxNotDeleting(botId);
801
- const cachedId = boxIdCache.get(botId);
824
+ export async function findBoat(cfg, botId) {
825
+ cfg = snapshotBoatConfig(cfg);
826
+ assertBotBoatNotDeleting(botId);
827
+ const cachedId = boatIdCache.get(botId);
802
828
  if (cachedId) {
803
829
  let direct = null;
804
830
  try {
805
- direct = await boxJson(cfg, `/boxes/${cachedId}`);
831
+ direct = await boatJson(cfg, `/boxes/${cachedId}`);
806
832
  }
807
833
  catch {
808
834
  // A direct read can fail while the account listing still succeeds.
809
835
  // Fall through to the authoritative paginated lookup before deciding.
810
836
  }
811
- const directBox = direct?.body?.box;
812
- if (direct?.ok && directBox?.id === cachedId && directBox.state !== "error")
813
- return directBox;
814
- if (direct?.ok && directBox?.id !== cachedId) {
837
+ const directBoat = direct?.body?.box;
838
+ if (direct?.ok && directBoat?.id === cachedId && directBoat.state !== "error")
839
+ return directBoat;
840
+ if (direct?.ok && directBoat?.id !== cachedId) {
815
841
  throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
816
842
  }
817
- boxIdCache.delete(botId); // gone or broken — fall back to the listing
843
+ boatIdCache.delete(botId); // gone or broken — fall back to the listing
818
844
  }
819
- const name = await boxNameFor(botId);
820
- const legacyName = legacyBoxNameFor(botId);
821
- const listed = await listBoxPages(cfg);
845
+ const name = await boatNameFor(botId);
846
+ const legacyName = legacyBoatNameFor(botId);
847
+ const listed = await listBoatPages(cfg);
822
848
  if (!listed.ok) {
823
849
  throw Object.assign(new Error(listed.problem), { status: 503 });
824
850
  }
825
851
  // Prefer the installation-scoped identity. A legacy name remains
826
852
  // discoverable only for this exact local bot id.
827
- const expected = listed.boxes.filter((candidate) => candidate?.name === name || candidate?.name === legacyName);
828
- if (expected.some((candidate) => !BOX_ID.test(candidate?.id))) {
853
+ const expected = listed.boats.filter((candidate) => candidate?.name === name || candidate?.name === legacyName);
854
+ if (expected.some((candidate) => !BOAT_ID.test(candidate?.id))) {
829
855
  throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
830
856
  }
831
857
  const found = expected.find((candidate) => candidate.name === name && candidate.state !== "error")
832
858
  ?? expected.find((candidate) => candidate.name === legacyName && candidate.state !== "error")
833
859
  ?? null;
834
860
  if (found) {
835
- const duplicateId = listed.boxes.filter((candidate) => candidate?.id === found.id).length !== 1;
861
+ const duplicateId = listed.boats.filter((candidate) => candidate?.id === found.id).length !== 1;
836
862
  if (duplicateId) {
837
863
  throw Object.assign(new Error("boat.dev returned a conflicting cloud computer identity"), { status: 503 });
838
864
  }
839
865
  if (found.name === legacyName)
840
- adoptResolvedBox(botId, found.id);
841
- boxIdCache.set(botId, found.id);
866
+ adoptResolvedBoat(botId, found.id);
867
+ boatIdCache.set(botId, found.id);
842
868
  }
843
869
  return found;
844
870
  }
845
- /** Ready-or-null without the LIST when we already know the box. */
846
- export async function readyBox(cfg, botId, budgetMs = 60_000) {
847
- cfg = snapshotBoxConfig(cfg);
848
- const box = await findBox(cfg, botId);
849
- if (!box)
871
+ /** Ready-or-null without the LIST when we already know the boat. */
872
+ export async function readyBoat(cfg, botId, budgetMs = 60_000) {
873
+ cfg = snapshotBoatConfig(cfg);
874
+ const boat = await findBoat(cfg, botId);
875
+ if (!boat)
850
876
  return null;
851
- if (READY.has(box.state))
852
- return box;
853
- return waitReady(cfg, box.id, budgetMs);
877
+ if (READY.has(boat.state))
878
+ return boat;
879
+ return waitReady(cfg, boat.id, budgetMs);
880
+ }
881
+ /** The Boat credential a request uses: the person's own token, else Cloud
882
+ * Pro's included one. Settings' own-key flows read cfg.box.token instead. */
883
+ export function boatAccount(cfg) {
884
+ return boatCredential(cfg.box?.token);
885
+ }
886
+ export function boatConfigured(cfg) {
887
+ return Boolean(boatAccount(cfg));
888
+ }
889
+ function usesIncludedBoat(cfg) {
890
+ return boatAccount(cfg)?.included === true;
854
891
  }
855
- export function boxConfigured(cfg) {
856
- return Boolean(cfg.box?.token);
892
+ /** What Settings shows: configured-or-not, and whether that is Cloud Pro's
893
+ * included account rather than a saved key. Never the token. */
894
+ export function describeBoatAccount(cfg) {
895
+ const account = boatAccount(cfg);
896
+ return { configured: Boolean(account), ...(account?.included ? { included: true } : {}) };
857
897
  }
858
898
  /** Ask the provider whether a token is real, before we let someone save
859
899
  * it. Without this the paste "succeeds", and the first sign of trouble is
860
- * a 401 in a different panel minutes later, with nothing to act on. */
900
+ * a 401 in a different panel minutes later, with nothing to act on. Only
901
+ * ever an own token, so only ever Boat itself. */
861
902
  export async function verifyToken(token) {
862
903
  try {
863
- const res = await fetch(`${BOX_API}/boxes`, {
904
+ const res = await fetch(`${boatProviderApi()}/boxes`, {
864
905
  headers: { authorization: `Bearer ${token}` },
865
906
  signal: AbortSignal.timeout(20_000),
866
907
  });
@@ -868,7 +909,7 @@ export async function verifyToken(token) {
868
909
  return { ok: true };
869
910
  if (res.status === 401 || res.status === 403) {
870
911
  // the common mistake is pasting some other credential entirely —
871
- // box API keys are prefixed, so say which thing is wrong
912
+ // boat API keys are prefixed, so say which thing is wrong
872
913
  return {
873
914
  ok: false,
874
915
  message: token.startsWith("box_")
@@ -882,18 +923,24 @@ export async function verifyToken(token) {
882
923
  return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
883
924
  }
884
925
  }
926
+ /** A rejected Cloud Pro relay token: nothing the person pasted, so nothing
927
+ * for them to fix in Settings. */
928
+ const INCLUDED_BOAT_UNAVAILABLE = "Cloud Pro's included cloud computers aren't available right now. Try again later.";
885
929
  /** Turn a provider refusal into something a person can act on. The
886
930
  * provider's own message is better than anything we can invent — it knows
887
931
  * the plan, the limit and the link — so prefer it and only fall back to
888
- * our own wording when it says nothing useful. */
889
- export function boxErrorMessage(status, what, body) {
932
+ * our own wording when it says nothing useful. `included`: the request used
933
+ * Cloud Pro's included token, not the person's own. */
934
+ export function boatErrorMessage(status, what, body, included = false) {
890
935
  const theirs = typeof body?.message === "string" ? body.message.trim() : "";
891
936
  const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
892
937
  if (status === 402) {
893
- // e.g. "Start the $20/month Box plan to create sandboxes."
894
- return [theirs || "boat.dev needs a paid Box plan before it will create a computer.", link].filter(Boolean).join(" ");
938
+ // e.g. "Start the $20/month Boat plan to create sandboxes."
939
+ return [theirs || "boat.dev needs a paid Boat plan before it will create a computer.", link].filter(Boolean).join(" ");
895
940
  }
896
941
  if (status === 401 || status === 403) {
942
+ if (included)
943
+ return INCLUDED_BOAT_UNAVAILABLE;
897
944
  return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
898
945
  }
899
946
  if (status === 429) {
@@ -905,26 +952,26 @@ export function boxErrorMessage(status, what, body) {
905
952
  * structured `trial_auto_stop_required` refusal. Retry that one condition
906
953
  * once at the provider's advertised maximum (or the documented two-hour
907
954
  * trial ceiling). Other create failures must retain their original error. */
908
- function trialBoxTtlSeconds(body) {
955
+ function trialBoatTtlSeconds(body) {
909
956
  const code = body?.error?.code ?? body?.code;
910
957
  if (code !== "trial_auto_stop_required")
911
958
  return null;
912
959
  const details = body?.error?.details ?? body?.details ?? {};
913
960
  for (const value of [details.maxTtlSeconds, details.maximumTtlSeconds, details.maxAutoStopSeconds]) {
914
- if (Number.isInteger(value) && value > 0 && value <= DEFAULT_BOX_TTL_SECONDS)
961
+ if (Number.isInteger(value) && value > 0 && value <= DEFAULT_BOAT_TTL_SECONDS)
915
962
  return value;
916
963
  }
917
- return TRIAL_BOX_TTL_SECONDS;
964
+ return TRIAL_BOAT_TTL_SECONDS;
918
965
  }
919
966
  function idempotentCreateInProgress(result) {
920
967
  const code = result.body?.error?.code ?? result.body?.code;
921
968
  return result.status === 409 && code === "idempotency_in_progress";
922
969
  }
923
970
  /** The keys this OpenMausBot already holds, as the environment its bots'
924
- * agents read on the box. The box is created with `noEnv: true`, so the
925
- * boat.dev account's own logins never land in the guest: the box has exactly
926
- * these and nothing else (see "Whose keys" in the Box integrated-agents docs). */
927
- export function boxCredentialEnv(cfg, env = process.env) {
971
+ * agents read on the boat. The boat is created with `noEnv: true`, so the
972
+ * boat.dev account's own logins never land in the guest: the boat has exactly
973
+ * these and nothing else (see "Whose keys" in the Boat integrated-agents docs). */
974
+ export function boatCredentialEnv(cfg, env = process.env) {
928
975
  const out = {};
929
976
  const put = (name, value) => {
930
977
  if (typeof value === "string" && value.trim())
@@ -933,13 +980,13 @@ export function boxCredentialEnv(cfg, env = process.env) {
933
980
  // The workspace key only: an ANTHROPIC_API_KEY in the server's own env is
934
981
  // never the workspace key (see loadConfig), so it is not forwarded either.
935
982
  put("ANTHROPIC_API_KEY", cfg.anthropic?.key);
936
- for (const name of BOX_FORWARDED_CREDENTIAL_ENV)
983
+ for (const name of BOAT_FORWARDED_CREDENTIAL_ENV)
937
984
  put(name, env[name]);
938
985
  return out;
939
986
  }
940
- /** Names the box's agents read (Claude Code, Codex, pi, OpenCode, Prime
987
+ /** Names the boat's agents read (Claude Code, Codex, pi, OpenCode, Prime
941
988
  * Agent, Kimi), forwarded verbatim from this server's environment when set. */
942
- const BOX_FORWARDED_CREDENTIAL_ENV = [
989
+ const BOAT_FORWARDED_CREDENTIAL_ENV = [
943
990
  "CLAUDE_CODE_OAUTH_TOKEN",
944
991
  "OPENAI_API_KEY",
945
992
  "OPENROUTER_API_KEY",
@@ -949,22 +996,22 @@ const BOX_FORWARDED_CREDENTIAL_ENV = [
949
996
  "KIMI_CODE_ACCESS_TOKEN",
950
997
  "KIMI_CODE_REFRESH_TOKEN",
951
998
  ];
952
- async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
999
+ async function requestBoatCreate(cfg, botId, ttlSeconds, env) {
953
1000
  // The computer needs the user's desktop session, not the account owner's
954
1001
  // host credentials. Keep provider-side env injection off; the only keys the
955
1002
  // guest ever has are the ones this OpenMausBot forwards (`env`), which its
956
- // agents need now that the turn runs on the box. The idempotency identity
1003
+ // agents need now that the turn runs on the boat. The idempotency identity
957
1004
  // stays the secret-free part: a trial-TTL retry must receive a different
958
1005
  // key, and the journal on disk never carries a credential.
959
1006
  const body = JSON.stringify({ ttlSeconds, noEnv: true });
960
1007
  const wireBody = JSON.stringify({ ttlSeconds, noEnv: true, ...(Object.keys(env).length ? { env } : {}) });
961
- let attempt = beginBoxCreate(botId, body);
1008
+ let attempt = beginBoatCreate(botId, body);
962
1009
  let request = attempt.request;
963
1010
  let createdThisAttempt = attempt.startedNow;
964
- // A previous process received the Box but died before naming it. Resolve
1011
+ // A previous process received the Boat but died before naming it. Resolve
965
1012
  // the durable identity directly; never issue a second create first.
966
1013
  if (request.boxId) {
967
- const recovered = await boxJson(cfg, `/boxes/${request.boxId}`, {
1014
+ const recovered = await boatJson(cfg, `/boxes/${request.boxId}`, {
968
1015
  signal: AbortSignal.timeout(20_000),
969
1016
  });
970
1017
  if (recovered.ok && recovered.body?.box?.id === request.boxId) {
@@ -973,8 +1020,8 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
973
1020
  if (recovered.status !== 404 && recovered.status !== 410) {
974
1021
  return { ...recovered, request, createdThisAttempt: false };
975
1022
  }
976
- discardBoxCreate(request);
977
- attempt = beginBoxCreate(botId, body);
1023
+ discardBoatCreate(request);
1024
+ attempt = beginBoatCreate(botId, body);
978
1025
  request = attempt.request;
979
1026
  createdThisAttempt = attempt.startedNow;
980
1027
  }
@@ -983,7 +1030,7 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
983
1030
  let inProgressRetries = 0;
984
1031
  for (;;) {
985
1032
  try {
986
- last = await boxJson(cfg, "/boxes", {
1033
+ last = await boatJson(cfg, "/boxes", {
987
1034
  method: "POST",
988
1035
  headers: { "Idempotency-Key": request.idempotencyKey },
989
1036
  signal: AbortSignal.timeout(45_000),
@@ -992,18 +1039,18 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
992
1039
  }
993
1040
  catch (error) {
994
1041
  // A dropped response is ambiguous: boat.dev may already have created
995
- // the Box. One retry with the same key recovers it safely.
1042
+ // the Boat. One retry with the same key recovers it safely.
996
1043
  if (ambiguousRetries++ === 0)
997
1044
  continue;
998
1045
  throw error;
999
1046
  }
1000
1047
  const boxId = last.body?.box?.id;
1001
1048
  if (last.ok && typeof boxId === "string" && boxId) {
1002
- request = rememberCreatedBox(request, boxId);
1049
+ request = rememberCreatedBoat(request, boxId);
1003
1050
  return { ...last, request, createdThisAttempt };
1004
1051
  }
1005
1052
  if (idempotentCreateInProgress(last)) {
1006
- const delay = BOX_CREATE_IN_PROGRESS_RETRY_DELAYS_MS[inProgressRetries++];
1053
+ const delay = BOAT_CREATE_IN_PROGRESS_RETRY_DELAYS_MS[inProgressRetries++];
1007
1054
  if (delay !== undefined) {
1008
1055
  await new Promise((resolve) => setTimeout(resolve, delay));
1009
1056
  continue;
@@ -1014,32 +1061,32 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
1014
1061
  continue;
1015
1062
  // A 5xx or any idempotency conflict can follow a provider-side create;
1016
1063
  // keep its key for recovery. Only a definitive client rejection proves
1017
- // this request did not create a Box and may be replaced safely.
1064
+ // this request did not create a Boat and may be replaced safely.
1018
1065
  if (last.status < 500 && last.status !== 409 && !last.ok)
1019
- discardBoxCreate(request);
1066
+ discardBoatCreate(request);
1020
1067
  return { ...last, request, createdThisAttempt };
1021
1068
  }
1022
1069
  }
1023
- async function createBox(cfg, botId, env) {
1024
- const first = await requestBoxCreate(cfg, botId, DEFAULT_BOX_TTL_SECONDS, env);
1070
+ async function createBoat(cfg, botId, env) {
1071
+ const first = await requestBoatCreate(cfg, botId, DEFAULT_BOAT_TTL_SECONDS, env);
1025
1072
  if (first.ok)
1026
1073
  return first;
1027
- const trialTtl = trialBoxTtlSeconds(first.body);
1028
- return trialTtl === null ? first : requestBoxCreate(cfg, botId, trialTtl, env);
1074
+ const trialTtl = trialBoatTtlSeconds(first.body);
1075
+ return trialTtl === null ? first : requestBoatCreate(cfg, botId, trialTtl, env);
1029
1076
  }
1030
1077
  /** A prior explicit delete always wins over provisioning. Reconcile/retry the
1031
1078
  * old immutable target, then require a fresh provision request so one click
1032
1079
  * can never both erase and silently recreate the same computer. */
1033
1080
  async function finishPriorDeletionBeforeProvision(cfg, botId) {
1034
- const remembered = boxDeletionSnapshot().filter((record) => record.ownerBotId === botId);
1081
+ const remembered = boatDeletionSnapshot().filter((record) => record.ownerBotId === botId);
1035
1082
  if (!remembered.length)
1036
1083
  return;
1037
1084
  for (const deletion of remembered) {
1038
- let state = await reconcileRecordedBoxDeletion(cfg, deletion, [0]);
1085
+ let state = await reconcileRecordedBoatDeletion(cfg, deletion, [0]);
1039
1086
  if (state !== "confirmed") {
1040
- const current = getBoxDeletion(deletion.boxId);
1087
+ const current = getBoatDeletion(deletion.boxId);
1041
1088
  if (current?.phase === "prepared" || current?.phase === "blocked") {
1042
- state = await requestRecordedBoxDeletion(cfg, {
1089
+ state = await requestRecordedBoatDeletion(cfg, {
1043
1090
  boxId: current.boxId,
1044
1091
  name: current.name,
1045
1092
  ownerBotId: current.ownerBotId,
@@ -1051,150 +1098,150 @@ async function finishPriorDeletionBeforeProvision(cfg, botId) {
1051
1098
  }
1052
1099
  throw Object.assign(new Error("the previous cloud computer deletion finished — retry to create a new computer"), { status: 409 });
1053
1100
  }
1054
- /** Box state for the Computer panel. */
1055
- export async function boxStatus(cfg, botId) {
1056
- cfg = snapshotBoxConfig(cfg);
1057
- if (!boxConfigured(cfg))
1101
+ /** Boat state for the Computer panel. */
1102
+ export async function boatStatus(cfg, botId) {
1103
+ cfg = snapshotBoatConfig(cfg);
1104
+ if (!boatConfigured(cfg))
1058
1105
  return { configured: false, box: null };
1059
- const box = await findBox(cfg, botId);
1106
+ const boat = await findBoat(cfg, botId);
1060
1107
  return {
1061
1108
  configured: true,
1062
- box: box ? { boxId: box.id, state: box.state, desktopAvailable: box.desktopAvailable ?? null } : null,
1109
+ box: boat ? { boxId: boat.id, state: boat.state, desktopAvailable: boat.desktopAvailable ?? null } : null,
1063
1110
  };
1064
1111
  }
1065
1112
  /**
1066
- * Find-or-create the bot's persistent box, wait for ready, and mint a fresh
1067
- * desktop URL. The box ships its own computer-use driver and agent runner.
1113
+ * Find-or-create the bot's persistent boat, wait for ready, and mint a fresh
1114
+ * desktop URL. The boat ships its own computer-use driver and agent runner.
1068
1115
  */
1069
- export async function provisionBox(cfg, botId, _botName) {
1070
- const credentialEnv = boxCredentialEnv(cfg);
1071
- cfg = snapshotBoxConfig(cfg);
1072
- if (!boxConfigured(cfg)) {
1116
+ export async function provisionBoat(cfg, botId, _botName) {
1117
+ const credentialEnv = boatCredentialEnv(cfg);
1118
+ cfg = snapshotBoatConfig(cfg);
1119
+ if (!boatConfigured(cfg)) {
1073
1120
  throw new Error('box provider not enabled — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
1074
1121
  }
1075
1122
  await finishPriorDeletionBeforeProvision(cfg, botId);
1076
- const vmName = await boxNameFor(botId);
1077
- let box = await findBox(cfg, botId);
1123
+ const vmName = await boatNameFor(botId);
1124
+ let boat = await findBoat(cfg, botId);
1078
1125
  let created = false;
1079
1126
  let createRequest = null;
1080
1127
  try {
1081
- if (!box) {
1128
+ if (!boat) {
1082
1129
  // Deletion can be prepared by another process after the initial lookup.
1083
1130
  // Never create a replacement until the durable fence is reconciled.
1084
- assertBotBoxNotDeleting(botId);
1131
+ assertBotBoatNotDeleting(botId);
1085
1132
  // Provider-side backstop: archives itself (billing pauses, disk
1086
1133
  // survives) if every stop path dies. Trial accounts get one narrower
1087
1134
  // retry when boat.dev reports their shorter TTL ceiling.
1088
- const createRes = await createBox(cfg, botId, credentialEnv);
1135
+ const createRes = await createBoat(cfg, botId, credentialEnv);
1089
1136
  if (!createRes.ok || !createRes.body?.box?.id) {
1090
- throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
1137
+ throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg)));
1091
1138
  }
1092
- box = createRes.body.box;
1139
+ boat = createRes.body.box;
1093
1140
  createRequest = createRes.request;
1094
1141
  created = createRes.createdThisAttempt;
1095
- const rename = await boxJson(cfg, `/boxes/${box.id}`, {
1142
+ const rename = await boatJson(cfg, `/boxes/${boat.id}`, {
1096
1143
  method: "PATCH",
1097
1144
  body: JSON.stringify({ name: vmName }),
1098
1145
  });
1099
1146
  if (!rename.ok)
1100
- throw new Error(boxErrorMessage(rename.status, "box naming", rename.body));
1147
+ throw new Error(boatErrorMessage(rename.status, "box naming", rename.body, usesIncludedBoat(cfg)));
1101
1148
  if (createRequest)
1102
- createRequest = resolveBoxCreate(createRequest);
1149
+ createRequest = resolveBoatCreate(createRequest);
1103
1150
  }
1104
- const ready = await waitReady(cfg, box.id);
1151
+ const ready = await waitReady(cfg, boat.id);
1105
1152
  if (!ready)
1106
1153
  throw new Error("box did not become ready within 90s — retry in a minute");
1107
- // Nothing to install: every box ships its own computer-use driver and
1154
+ // Nothing to install: every boat ships its own computer-use driver and
1108
1155
  // registers it with every harness it runs.
1109
- const joinUrl = await mintDesktopUrl(cfg, box.id);
1156
+ const joinUrl = await mintDesktopUrl(cfg, boat.id);
1110
1157
  if (!joinUrl)
1111
1158
  throw new Error("box desktop link could not be created");
1112
- return { boxId: box.id, machineName: vmName, reused: !created, state: ready.state, joinUrl };
1159
+ return { boxId: boat.id, machineName: vmName, reused: !created, state: ready.state, joinUrl };
1113
1160
  }
1114
1161
  catch (error) {
1115
- if (!created || !box?.id)
1162
+ if (!created || !boat?.id)
1116
1163
  throw error;
1117
1164
  const originalMessage = error instanceof Error ? error.message : String(error);
1118
1165
  // Capture the provider's current name when possible. Naming may be the
1119
1166
  // step that failed, so the desired deterministic name is only a fallback
1120
1167
  // for the durable fence, never proof of a later live identity.
1121
- const inspected = await inspectBoxIdentity(cfg, box.id);
1168
+ const inspected = await inspectBoatIdentity(cfg, boat.id);
1122
1169
  if (inspected.available && !inspected.identity) {
1123
- retireDeletedBoxCreate(box.id);
1124
- boxIdCache.delete(botId);
1170
+ retireDeletedBoatCreate(boat.id);
1171
+ boatIdCache.delete(botId);
1125
1172
  throw error;
1126
1173
  }
1127
1174
  let cleanupConfirmation;
1128
1175
  try {
1129
- cleanupConfirmation = await requestRecordedBoxDeletion(cfg, {
1130
- boxId: box.id,
1176
+ cleanupConfirmation = await requestRecordedBoatDeletion(cfg, {
1177
+ boxId: boat.id,
1131
1178
  name: inspected.identity?.name ?? vmName,
1132
1179
  ownerBotId: botId,
1133
1180
  });
1134
1181
  }
1135
1182
  catch (cleanupError) {
1136
1183
  const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
1137
- throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in boat.dev.`);
1184
+ throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${boat.id} in boat.dev.`);
1138
1185
  }
1139
1186
  if (cleanupConfirmation === "confirmed")
1140
1187
  throw error;
1141
- boxIdCache.delete(botId);
1188
+ boatIdCache.delete(botId);
1142
1189
  throw Object.assign(new Error(`${originalMessage}. boat.dev accepted deletion of the new computer, but it is still pending; `
1143
- + `its recovery record was kept, along with its deletion fence. Check box ${box.id} in boat.dev.`, { cause: error }), { status: 503 });
1190
+ + `its recovery record was kept, along with its deletion fence. Check box ${boat.id} in boat.dev.`, { cause: error }), { status: 503 });
1144
1191
  }
1145
1192
  }
1146
- /** Wake the bot's box and return a FRESH desktop URL. */
1147
- export async function joinBox(cfg, botId) {
1148
- cfg = snapshotBoxConfig(cfg);
1149
- const box = await findBox(cfg, botId);
1150
- if (!box)
1193
+ /** Wake the bot's boat and return a FRESH desktop URL. */
1194
+ export async function joinBoat(cfg, botId) {
1195
+ cfg = snapshotBoatConfig(cfg);
1196
+ const boat = await findBoat(cfg, botId);
1197
+ if (!boat)
1151
1198
  throw new Error("no computer yet — provision it first");
1152
- const ready = await waitReady(cfg, box.id);
1199
+ const ready = await waitReady(cfg, boat.id);
1153
1200
  if (!ready)
1154
1201
  throw new Error("the box did not wake in time — try again");
1155
- // Provider archive/resume preserves disk but not processes; the box brings
1202
+ // Provider archive/resume preserves disk but not processes; the boat brings
1156
1203
  // its own driver daemon back up, so there is nothing to reattach here.
1157
- return { joinUrl: await mintDesktopUrl(cfg, box.id), state: ready.state ?? null };
1204
+ return { joinUrl: await mintDesktopUrl(cfg, boat.id), state: ready.state ?? null };
1158
1205
  }
1159
1206
  /** Mint a human-control URL without changing provider lifecycle or guest
1160
1207
  * processes. This is the only join path allowed while a bot turn is active. */
1161
- export async function joinReadyBox(cfg, botId) {
1162
- cfg = snapshotBoxConfig(cfg);
1163
- const box = await findBox(cfg, botId);
1164
- if (!box)
1208
+ export async function joinReadyBoat(cfg, botId) {
1209
+ cfg = snapshotBoatConfig(cfg);
1210
+ const boat = await findBoat(cfg, botId);
1211
+ if (!boat)
1165
1212
  throw Object.assign(new Error("no computer yet — provision it first"), { status: 409 });
1166
- if (!READY.has(box.state)) {
1213
+ if (!READY.has(boat.state)) {
1167
1214
  throw Object.assign(new Error("the cloud computer is sleeping or starting — interrupt the bot before waking it"), { status: 409 });
1168
1215
  }
1169
- return { joinUrl: await mintDesktopUrl(cfg, box.id), state: box.state ?? null };
1216
+ return { joinUrl: await mintDesktopUrl(cfg, boat.id), state: boat.state ?? null };
1170
1217
  }
1171
- /** Archive the bot's box now (billing pauses, disk survives). */
1172
- export async function sleepBox(cfg, botId) {
1173
- cfg = snapshotBoxConfig(cfg);
1174
- const box = await findBox(cfg, botId);
1175
- if (!box)
1218
+ /** Archive the bot's boat now (billing pauses, disk survives). */
1219
+ export async function sleepBoat(cfg, botId) {
1220
+ cfg = snapshotBoatConfig(cfg);
1221
+ const boat = await findBoat(cfg, botId);
1222
+ if (!boat)
1176
1223
  throw new Error("no computer for this bot");
1177
- await stopBox(cfg, box.id);
1178
- forgetBoxId(box.id);
1224
+ await stopBoat(cfg, boat.id);
1225
+ forgetBoatId(boat.id);
1179
1226
  return { ok: true };
1180
1227
  }
1181
1228
  /** Owner-scoped shell for the Computer panel's console. */
1182
- export async function execOnBox(cfg, botId, command) {
1183
- cfg = snapshotBoxConfig(cfg);
1229
+ export async function execOnBoat(cfg, botId, command) {
1230
+ cfg = snapshotBoatConfig(cfg);
1184
1231
  if (command.length > MAX_REMOTE_COMMAND_LENGTH) {
1185
1232
  throw new RangeError(`command is too long (maximum ${MAX_REMOTE_COMMAND_LENGTH} characters)`);
1186
1233
  }
1187
- const box = await findBox(cfg, botId);
1188
- if (!box)
1234
+ const boat = await findBoat(cfg, botId);
1235
+ if (!boat)
1189
1236
  throw new Error("no computer for this bot yet");
1190
- const ready = await waitReady(cfg, box.id, 60_000);
1237
+ const ready = await waitReady(cfg, boat.id, 60_000);
1191
1238
  if (!ready)
1192
1239
  throw new Error("box did not wake");
1193
- const out = await runCommand(cfg, box.id, isolatedRemoteCommand(command));
1240
+ const out = await runCommand(cfg, boat.id, isolatedRemoteCommand(command));
1194
1241
  return { exitCode: out.exitCode, stdout: out.stdout.slice(-4000), stderr: out.stderr.slice(-2000) };
1195
1242
  }
1196
1243
  // Screenshot for the Computer panel + screen-in-chat. Two hops: capture
1197
- // to a file on the box (scrot straight to JPEG — no ImageMagick startup
1244
+ // to a file on the boat (scrot straight to JPEG — no ImageMagick startup
1198
1245
  // unless a downscale is actually needed), then read the bytes back.
1199
1246
  // Base64 over command stdout is NOT reliable for the panel's full-size
1200
1247
  // frames (probed 2026-08-12: an otherwise-complete payload came back with
@@ -1203,7 +1250,7 @@ export async function execOnBox(cfg, botId, command) {
1203
1250
  // The frame is for a person: it fills the panel and opens in the chat's
1204
1251
  // image viewer, so it keeps the desktop's native size up to 1080p and a
1205
1252
  // quality where page text stays legible. (Sizing it is now the only say
1206
- // OpenMausBot has over any frame off this box: the turn runs on the box's
1253
+ // OpenMausBot has over any frame off this box: the turn runs on the boat's
1207
1254
  // own agent, so the model's own captures never pass through here.) Only
1208
1255
  // wider displays are scaled down, with -resize rather than -thumbnail so
1209
1256
  // the resample is not the fast-and-blurry kind meant for icons. The
@@ -1215,7 +1262,7 @@ export const PANEL_FRAME_WIDTH = 1920;
1215
1262
  export const PANEL_FRAME_QUALITY = 85;
1216
1263
  // ffmpeg's -q:v runs 2 (best) to 31; 3 lands near JPEG quality 85.
1217
1264
  const PANEL_FRAME_FFMPEG_Q = 3;
1218
- /** The shell that captures one panel frame on the box. Exported for tests. */
1265
+ /** The shell that captures one panel frame on the boat. Exported for tests. */
1219
1266
  export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FRAME_QUALITY, framePath = PANEL_PATH, nativeSize = false } = {}) {
1220
1267
  return [
1221
1268
  "export DISPLAY=${DISPLAY:-:0}",
@@ -1230,18 +1277,18 @@ export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FR
1230
1277
  ].join("; ");
1231
1278
  }
1232
1279
  const SHOT_CMD = panelShotCommand();
1233
- // A compromised box can answer with an arbitrarily large "frame"; cap what
1280
+ // A compromised boat can answer with an arbitrarily large "frame"; cap what
1234
1281
  // the server ever buffers for one (raw bytes, before base64) so a single
1235
1282
  // response cannot exhaust memory.
1236
1283
  const MAX_FRAME_BYTES = 8 * 1024 * 1024;
1237
1284
  const FRAME_TOO_LARGE = "the box frame exceeds the 8 MB limit";
1238
- /** Read a file off the box as base64 — raw artifact bytes when the API
1285
+ /** Read a file off the boat as base64 — raw artifact bytes when the API
1239
1286
  * supports it (33% less transfer, no JSON envelope), else the files API. */
1240
1287
  async function readFileBase64(cfg, boxId, path, signal) {
1241
1288
  let bytes = null;
1242
1289
  let tooLarge = false;
1243
1290
  try {
1244
- const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`, { signal });
1291
+ const res = await boatFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`, { signal });
1245
1292
  if (res.ok) {
1246
1293
  const declaredLength = res.headers.get("content-length");
1247
1294
  if (declaredLength !== null && Number(declaredLength) > MAX_FRAME_BYTES)
@@ -1259,7 +1306,7 @@ async function readFileBase64(cfg, boxId, path, signal) {
1259
1306
  if (bytes?.length)
1260
1307
  return bytes.toString("base64");
1261
1308
  signal?.throwIfAborted();
1262
- const { ok, body } = await boxJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`, { signal });
1309
+ const { ok, body } = await boatJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`, { signal });
1263
1310
  const content = body?.content;
1264
1311
  if (ok && typeof content === "string" && content) {
1265
1312
  if (Buffer.byteLength(content, "base64") > MAX_FRAME_BYTES)
@@ -1268,18 +1315,18 @@ async function readFileBase64(cfg, boxId, path, signal) {
1268
1315
  }
1269
1316
  return null;
1270
1317
  }
1271
- /** `knownBoxId` skips box resolution entirely — the screen poller holds
1318
+ /** `knownBoatId` skips boat resolution entirely — the screen poller holds
1272
1319
  * the id for the whole turn and must not re-resolve it every frame. */
1273
- export async function screenshotBox(cfg, botId, knownBoxId, options) {
1274
- cfg = snapshotBoxConfig(cfg);
1275
- let boxId = knownBoxId;
1320
+ export async function screenshotBoat(cfg, botId, knownBoatId, options) {
1321
+ cfg = snapshotBoatConfig(cfg);
1322
+ let boxId = knownBoatId;
1276
1323
  if (!boxId) {
1277
- const box = await findBox(cfg, botId);
1278
- if (!box)
1324
+ const boat = await findBoat(cfg, botId);
1325
+ if (!boat)
1279
1326
  throw new Error("no computer for this bot yet");
1280
- if (!READY.has(box.state))
1281
- throw new Error(`box is ${box.state}`);
1282
- boxId = box.id;
1327
+ if (!READY.has(boat.state))
1328
+ throw new Error(`box is ${boat.state}`);
1329
+ boxId = boat.id;
1283
1330
  }
1284
1331
  const framePath = options?.nativeSize ? PANEL_PATH + ".model.jpg" : PANEL_PATH;
1285
1332
  const out = await runCommand(cfg, boxId, options?.nativeSize ? panelShotCommand({ framePath, nativeSize: true }) : SHOT_CMD, { timeoutMs: 60_000, signal: options?.signal });