openmausbot 0.1.89 → 0.1.90

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