openmausbot 0.1.90 → 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 (112) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +18 -3
  61. package/dist-server/container-mcp.js +84 -32
  62. package/dist-server/drivers/agents-proxy.js +11 -0
  63. package/dist-server/index.js +25183 -23580
  64. package/dist-server/openmausbot.js +313 -159
  65. package/dist-server/pair-cli.js +313 -159
  66. package/dist-server/server/boat.js +66 -21
  67. package/dist-server/server/bot-overview.js +7 -4
  68. package/dist-server/server/cloud-home-start.js +23 -5
  69. package/dist-server/server/cloud-home.js +25 -2
  70. package/dist-server/server/cloud-lending.js +103 -0
  71. package/dist-server/server/cloud-move-http.js +268 -0
  72. package/dist-server/server/cloud-move.js +418 -0
  73. package/dist-server/server/config.js +42 -4
  74. package/dist-server/server/decider/index.js +238 -0
  75. package/dist-server/server/decider/jev.js +183 -0
  76. package/dist-server/server/decider/log.js +62 -0
  77. package/dist-server/server/decider/relay.js +52 -0
  78. package/dist-server/server/decider/room-routing.js +88 -0
  79. package/dist-server/server/decider/types.js +5 -0
  80. package/dist-server/server/drivers/agents-catalog.js +16 -0
  81. package/dist-server/server/drivers/boatagent.js +20 -12
  82. package/dist-server/server/drivers/claude.js +1 -0
  83. package/dist-server/server/group-goal-run.js +6 -4
  84. package/dist-server/server/harness-capabilities.js +2 -0
  85. package/dist-server/server/included-services.js +70 -0
  86. package/dist-server/server/index.js +438 -102
  87. package/dist-server/server/model-context-window.js +4 -4
  88. package/dist-server/server/package-export.js +17 -10
  89. package/dist-server/server/package-import.js +6 -3
  90. package/dist-server/server/package-parts.js +3 -1
  91. package/dist-server/server/request-auth.js +2 -0
  92. package/dist-server/server/routes/decider.js +19 -0
  93. package/dist-server/server/shared-computers.js +116 -30
  94. package/dist-server/server/store.js +17 -2
  95. package/dist-server/server/surface.js +13 -0
  96. package/dist-server/server/system-prompt.js +11 -0
  97. package/dist-server/server/team-backup.js +6 -2
  98. package/dist-server/server/team-manifest.js +11 -3
  99. package/dist-server/server/tts/elevenlabs.js +11 -6
  100. package/dist-server/server/tts/index.js +17 -10
  101. package/dist-server/server/workspace-backup-policy.js +22 -3
  102. package/dist-server/server/workspace-backup.js +25 -8
  103. package/dist-server/shared/cloud-home.js +4 -0
  104. package/dist-server/shared/package-format.js +30 -4
  105. package/dist-server/shared/team-backup.js +20 -3
  106. package/dist-server/vps-container-mcp.js +84 -32
  107. package/dist-server/workspace-backup.worker.js +113 -31
  108. package/package.json +1 -1
  109. package/dist/assets/channel-BCuyHjFV.js +0 -1
  110. package/dist/assets/index-CRvo-DS2.css +0 -1
  111. package/dist/assets/index-CWYGlA63.js +0 -588
  112. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -13,6 +13,7 @@
13
13
  import { createHash } from "node:crypto";
14
14
  import { DATA_DIR } from "./config.js";
15
15
  import { loadEnvironmentId } from "./environment.js";
16
+ import { boatCredential, boatProviderApi } from "./included-services.js";
16
17
  import { adoptResolvedBoat, beginBoatCreate, boatCreateRecoverySnapshot, discardBoatCreate, rememberCreatedBoat, resolveBoatCreate, retireDeletedBoatCreate, } from "./boat-create-idempotency.js";
17
18
  import { boatDeletionSnapshot, getBoatDeletion, hasPendingBoatDeletionForBot, markBoatDeletionAccepted, markBoatDeletionBlocked, prepareBoatDeletion, retireBoatDeletion, } from "./boat-delete-journal.js";
18
19
  const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
@@ -34,10 +35,8 @@ 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
38
  // Boat's provider surface keeps its historical Box-era names: env OMB_BOX_API,
39
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";
41
40
  const READY = new Set(["idle", "ready", "running"]);
42
41
  const SLEEPING = new Set(["archived", "archiving", "stopped", "stopping"]);
43
42
  const DEFAULT_BOAT_TTL_SECONDS = 8 * 60 * 60;
@@ -105,11 +104,15 @@ export function boatTurnLifecycleAction({ explicitCloud, canMount, state, }) {
105
104
  function snapshotBoatConfig(cfg) {
106
105
  return { box: cfg.box ? { token: cfg.box.token } : undefined };
107
106
  }
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. */
108
110
  function boatFetch(cfg, path, opts = {}) {
109
- return fetch(`${BOAT_API}${path}`, {
111
+ const account = boatAccount(cfg);
112
+ return fetch(`${account?.api ?? boatProviderApi()}${path}`, {
110
113
  ...opts,
111
114
  headers: {
112
- authorization: `Bearer ${cfg.box?.token}`,
115
+ authorization: `Bearer ${account?.token}`,
113
116
  "content-type": "application/json",
114
117
  ...opts.headers,
115
118
  },
@@ -280,7 +283,7 @@ async function requestRecordedBoatDeletion(cfg, identity, pollDelaysMs = BOAT_DE
280
283
  }
281
284
  if (!removed.ok) {
282
285
  markBoatDeletionBlocked(identity.boxId);
283
- throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body)), { status: removed.status });
286
+ throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body, usesIncludedBoat(cfg))), { status: removed.status });
284
287
  }
285
288
  const confirmation = await confirmAcceptedBoatDeletion(cfg, deletion, removed.body, pollDelaysMs);
286
289
  if (confirmation === "blocked")
@@ -346,6 +349,8 @@ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
346
349
  async function waitReady(cfg, boxId, budgetMs = 90_000) {
347
350
  assertBoatNotDeleting(boxId);
348
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;
349
354
  while (Date.now() - t0 < budgetMs) {
350
355
  assertBoatNotDeleting(boxId);
351
356
  const { body } = await boatJson(cfg, `/boxes/${boxId}`);
@@ -354,11 +359,25 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
354
359
  return body.box;
355
360
  if (state === "error")
356
361
  return null;
357
- // an archiving boat can't resume until the snapshot lands — nudge after
358
- if (state === "archived")
359
- await boatJson(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
+ }
360
377
  await new Promise((r) => setTimeout(r, 2500));
361
378
  }
379
+ if (resumeFailure)
380
+ throw new Error(resumeFailure);
362
381
  return null;
363
382
  }
364
383
  // Resolving a bot's boat means LISTing every boat in the account, so it is
@@ -367,9 +386,9 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
367
386
  // whenever the direct read fails (deleted/renamed boat) and always carries
368
387
  // the live state so callers can still see "archived".
369
388
  const boatIdCache = new Map();
370
- function boatInventoryProblem(status, body) {
389
+ function boatInventoryProblem(status, body, included = false) {
371
390
  if (status === 401 || status === 403) {
372
- return "boat.dev rejected the Boat 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";
373
392
  }
374
393
  if (status === 429)
375
394
  return "boat.dev is rate-limiting this account — wait a minute and refresh";
@@ -396,7 +415,11 @@ async function listBoatPages(cfg) {
396
415
  return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
397
416
  }
398
417
  if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
399
- return { ok: false, problem: boatInventoryProblem(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
+ };
400
423
  }
401
424
  boats.push(...listed.body.boxes);
402
425
  const next = listed.body?.pageInfo?.nextCursor;
@@ -672,7 +695,7 @@ export async function inspectBoatIdentity(cfg, boxId) {
672
695
  return { available: true, identity: null, problem: null };
673
696
  }
674
697
  if (!inspected.ok) {
675
- return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body) };
698
+ return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body, usesIncludedBoat(cfg)) };
676
699
  }
677
700
  const candidate = inspected.body?.box;
678
701
  const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
@@ -722,8 +745,9 @@ async function stopBoat(cfg, boxId) {
722
745
  // must never say a computer is sleeping when boat.dev rejected the action.
723
746
  await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
724
747
  const stopped = await boatJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
725
- if (!stopped.ok)
726
- throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body)), { status: stopped.status });
748
+ if (!stopped.ok) {
749
+ throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body, usesIncludedBoat(cfg))), { status: stopped.status });
750
+ }
727
751
  }
728
752
  function forgetBoatId(boxId) {
729
753
  for (const [botId, cachedId] of boatIdCache) {
@@ -854,15 +878,30 @@ export async function readyBoat(cfg, botId, budgetMs = 60_000) {
854
878
  return boat;
855
879
  return waitReady(cfg, boat.id, budgetMs);
856
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
+ }
857
886
  export function boatConfigured(cfg) {
858
- return Boolean(cfg.box?.token);
887
+ return Boolean(boatAccount(cfg));
888
+ }
889
+ function usesIncludedBoat(cfg) {
890
+ return boatAccount(cfg)?.included === true;
891
+ }
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 } : {}) };
859
897
  }
860
898
  /** Ask the provider whether a token is real, before we let someone save
861
899
  * it. Without this the paste "succeeds", and the first sign of trouble is
862
- * 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. */
863
902
  export async function verifyToken(token) {
864
903
  try {
865
- const res = await fetch(`${BOAT_API}/boxes`, {
904
+ const res = await fetch(`${boatProviderApi()}/boxes`, {
866
905
  headers: { authorization: `Bearer ${token}` },
867
906
  signal: AbortSignal.timeout(20_000),
868
907
  });
@@ -884,11 +923,15 @@ export async function verifyToken(token) {
884
923
  return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
885
924
  }
886
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.";
887
929
  /** Turn a provider refusal into something a person can act on. The
888
930
  * provider's own message is better than anything we can invent — it knows
889
931
  * the plan, the limit and the link — so prefer it and only fall back to
890
- * our own wording when it says nothing useful. */
891
- export function boatErrorMessage(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) {
892
935
  const theirs = typeof body?.message === "string" ? body.message.trim() : "";
893
936
  const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
894
937
  if (status === 402) {
@@ -896,6 +939,8 @@ export function boatErrorMessage(status, what, body) {
896
939
  return [theirs || "boat.dev needs a paid Boat plan before it will create a computer.", link].filter(Boolean).join(" ");
897
940
  }
898
941
  if (status === 401 || status === 403) {
942
+ if (included)
943
+ return INCLUDED_BOAT_UNAVAILABLE;
899
944
  return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
900
945
  }
901
946
  if (status === 429) {
@@ -1089,7 +1134,7 @@ export async function provisionBoat(cfg, botId, _botName) {
1089
1134
  // retry when boat.dev reports their shorter TTL ceiling.
1090
1135
  const createRes = await createBoat(cfg, botId, credentialEnv);
1091
1136
  if (!createRes.ok || !createRes.body?.box?.id) {
1092
- throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body));
1137
+ throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg)));
1093
1138
  }
1094
1139
  boat = createRes.body.box;
1095
1140
  createRequest = createRes.request;
@@ -1099,7 +1144,7 @@ export async function provisionBoat(cfg, botId, _botName) {
1099
1144
  body: JSON.stringify({ name: vmName }),
1100
1145
  });
1101
1146
  if (!rename.ok)
1102
- throw new Error(boatErrorMessage(rename.status, "box naming", rename.body));
1147
+ throw new Error(boatErrorMessage(rename.status, "box naming", rename.body, usesIncludedBoat(cfg)));
1103
1148
  if (createRequest)
1104
1149
  createRequest = resolveBoatCreate(createRequest);
1105
1150
  }
@@ -144,14 +144,17 @@ export function grantsSummary(connectorTools) {
144
144
  })
145
145
  .sort((a, b) => (a.slug < b.slug ? -1 : 1));
146
146
  }
147
- function computerReach(computer) {
147
+ function computerReach(computer, cloudHome = false) {
148
+ // A Cloud home never offers either; say so rather than list a preference
149
+ // that every task there refuses.
150
+ const unavailable = cloudHome ? ", which isn't available on OMB Cloud" : "";
148
151
  switch (computer) {
149
152
  case "cloud":
150
153
  return "Computer preference: cloud computer.";
151
154
  case "vm":
152
- return "Computer preference: Local VM.";
155
+ return `Computer preference: Local VM${unavailable}.`;
153
156
  case "local":
154
- return "Computer preference: this computer.";
157
+ return `Computer preference: this computer${unavailable}.`;
155
158
  case "browser":
156
159
  return "Computer preference: browser only.";
157
160
  case "off":
@@ -162,7 +165,7 @@ function computerReach(computer) {
162
165
  }
163
166
  function reachesLines(facts) {
164
167
  const lines = [];
165
- const computer = computerReach(facts.bot.computer);
168
+ const computer = computerReach(facts.bot.computer, facts.cloudHome);
166
169
  if (computer)
167
170
  lines.push(computer);
168
171
  lines.push(facts.bot.cwd ? `Works in ${facts.bot.cwd}.` : "Works in its private workspace.");
@@ -6,12 +6,13 @@
6
6
  // webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080. The edge is
7
7
  // the only listener the network can reach, and it always forwards with
8
8
  // X-Forwarded-*, so request-auth.ts never grants a remote request loopback
9
- // trust. If either child exits, both stop and the machine restarts.
9
+ // trust. If either child exits, both stop and the machine restarts; the one
10
+ // exception is the server asking to be started again after a restore.
10
11
  import { spawn } from "node:child_process";
11
12
  import { chownSync, readFileSync, statSync } from "node:fs";
12
13
  import { dirname, join, posix, resolve } from "node:path";
13
14
  import { fileURLToPath, pathToFileURL } from "node:url";
14
- import { cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
15
+ import { CLOUD_HOME_RESTART_EXIT_CODE, cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
15
16
  const SERVICE_USER = "maus";
16
17
  /** uid/gid from /etc/passwd; Node has no getpwnam. */
17
18
  export function passwdIds(passwd, name) {
@@ -39,6 +40,11 @@ export function cloudHomeChildEnvironments(config, env, home) {
39
40
  };
40
41
  return { server, edge };
41
42
  }
43
+ /** What the launcher does when the server child exits: start it again only
44
+ * when it asked to (a committed restore), and only a few times in a row. */
45
+ export function serverExitAction(code, stopping, restarts) {
46
+ return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
47
+ }
42
48
  export function startCloudHome(env = process.env) {
43
49
  process.umask(0o077);
44
50
  const config = cloudHomeConfiguration(env);
@@ -81,13 +87,25 @@ export function startCloudHome(env = process.env) {
81
87
  child.kill("SIGKILL"); }, 20_000);
82
88
  force.unref();
83
89
  };
84
- const run = (command, args, childEnv) => {
90
+ const run = (command, args, childEnv, again) => {
85
91
  const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
86
92
  children.push(child);
87
93
  child.once("error", () => stop(true));
88
- child.once("exit", () => stop(true));
94
+ child.once("exit", (code) => {
95
+ children.splice(children.indexOf(child), 1);
96
+ if (!again?.(code))
97
+ stop(true);
98
+ });
89
99
  };
90
- run(process.execPath, [join(here, "index.js")], server);
100
+ let restarts = 0;
101
+ const runServer = () => run(process.execPath, [join(here, "index.js")], server, (code) => {
102
+ if (serverExitAction(code, stopping, restarts) !== "restart")
103
+ return false;
104
+ restarts++;
105
+ runServer();
106
+ return true;
107
+ });
108
+ runServer();
91
109
  run(env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy", ["run", "--config", env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile", "--adapter", "caddyfile"], edge);
92
110
  process.once("SIGTERM", () => stop(false));
93
111
  process.once("SIGINT", () => stop(false));
@@ -1,5 +1,5 @@
1
- // OMB Cloud Pro home machine: the boot contract, the Admin's signed pairing
2
- // request, and the volume the machine lives on. docs/cloud-pro.md is the
1
+ // OMB Cloud Pro home machine: the boot contract, the places it offers, the
2
+ // Admin's signed pairing request, and the volume the machine lives on. docs/cloud-pro.md is the
3
3
  // contract of record (and openmaus-cloud docs/consumer-cloud.md its Admin
4
4
  // half); keep them in step.
5
5
  //
@@ -39,6 +39,10 @@ export const CLOUD_PAIRING_SKEW_S = 300;
39
39
  export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
40
40
  const MAX_NONCES = 10_000;
41
41
  export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
42
+ /** The server exits with this after a restore commits (Move to Cloud): the
43
+ * launcher then starts it again, and startup installs the restore. Any other
44
+ * exit stops the machine for Fly to restart. */
45
+ export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
42
46
  export function cloudHomeConfigured(env = process.env) {
43
47
  return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
44
48
  }
@@ -92,6 +96,25 @@ export function withoutIgnoredCloudKeys(env) {
92
96
  delete kept[key];
93
97
  return kept;
94
98
  }
99
+ // The places a Cloud home offers live in shared/cloud-home.ts, so the app
100
+ // lists exactly what the server accepts.
101
+ export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
102
+ /** Why a Cloud home refuses a place it never offers (no "this computer" of
103
+ * the person's, no Local VM), in the words the person reads; undefined for a
104
+ * place it offers. A turn's error shows 160 characters, so each fits. */
105
+ export function cloudHomePlaceRefusal(place) {
106
+ if (place === "local")
107
+ return "This computer isn't a place on your OMB Cloud: its bots run in the cloud. Set Works on to Auto, Cloud or Browser, or lend your Mac under Settings → OMB Cloud.";
108
+ if (place === "vm")
109
+ return "Bots on your OMB Cloud can't use a Local VM: the cloud machine has no container runtime. Set Works on to Auto, Cloud or Browser.";
110
+ return undefined;
111
+ }
112
+ /** What a turn is told when Cloud is chosen but no Boat account is set up (no
113
+ * key of the person's and no included Boat). A Cloud home has no Local VM to
114
+ * suggest instead. */
115
+ export function boatNotConfiguredMessage(cloudHome) {
116
+ return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
117
+ }
95
118
  /** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
96
119
  export function cloudPairingSignature(secret, timestamp, nonce, body) {
97
120
  const bodyHash = createHash("sha256").update(body).digest("base64url");
@@ -0,0 +1,103 @@
1
+ // Who may use a Mac lent to an OMB Cloud home (docs/cloud-pro.md, "Let my
2
+ // Cloud use this Mac"). A Cloud home is one person's server, but not every
3
+ // turn on it is theirs: a guest the owner paired, a webhook's payload, or a
4
+ // line someone else slipped into a running turn must never reach the Mac.
5
+ //
6
+ // A turn may use the lent Mac only when the harness can prove it acts for the
7
+ // owner:
8
+ // - a conversation the owner started from one of their own devices (a live
9
+ // session with admin scope), or
10
+ // - a scheduled run of a routine whose instructions the owner wrote (created
11
+ // or edited from one of those devices), or a run the owner started by hand;
12
+ // and the conversation holds nobody else's words, ever: a resumed session
13
+ // carries everything said in it, so one line (or card answer) from a guest,
14
+ // a teammate bot or a local process, before or during the turn, takes that
15
+ // conversation out of lending. The owner starts a new one to lend again.
16
+ // Webhook runs, guests, rooms, a bot's delegated or peer turn, and anything
17
+ // unprovable never can.
18
+ // Everything here is pure; server/index.ts supplies the records.
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, lstatSync, readFileSync } from "node:fs";
21
+ import { z } from "zod";
22
+ import { writeFileAtomic } from "./atomic.js";
23
+ const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
24
+ export function cloudHomeTurnMayLend(turn) {
25
+ return cloudHomeLendingRefusal(turn) === null;
26
+ }
27
+ export function cloudHomeLendingRefusal(turn) {
28
+ const { request } = turn;
29
+ if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
30
+ return "unproven";
31
+ const index = turn.thread.findIndex(line => line.id === request.messageId);
32
+ if (index < 0)
33
+ return "unproven";
34
+ let owner;
35
+ if (request.automation === undefined) {
36
+ owner = turn.ownerPerson(personOf(turn.thread[index]));
37
+ }
38
+ else {
39
+ const run = turn.routineRun();
40
+ owner = request.automation !== "webhook" && Boolean(run) && run.triggerSource !== "webhook" &&
41
+ run.ownerAuthored && (run.triggerSource !== "manual" || run.ownerStarted);
42
+ }
43
+ if (!owner)
44
+ return "not-owner";
45
+ // Nothing anyone else wrote (a guest, a teammate bot, a local process) may
46
+ // be anywhere in this conversation, before or after the request, as a line
47
+ // (sent, queued, steered or handed in) or as the answer to a card. A
48
+ // routine's own prompt line names nobody; its provenance was proven above.
49
+ const clean = turn.thread.every((line, at) => (line.role !== "user" || (at === index && request.automation !== undefined) || turn.ownerPerson(personOf(line))) &&
50
+ (!answeredByPerson(line.card) || turn.ownerPerson(turn.cardAnswerer(line.card))));
51
+ return clean ? null : "someone-else";
52
+ }
53
+ /** A card someone answered: a person's verdict or words. The harness's own
54
+ * settlements (automatic approvals, a turn that ended) are marked dismissed
55
+ * or unavailable and carry nobody's words. */
56
+ function answeredByPerson(card) {
57
+ if (!card)
58
+ return false;
59
+ return (Boolean(card.answered) && card.answered !== "unavailable" && card.dismissed !== true) || Boolean(card.answeredText);
60
+ }
61
+ /** What a routine run does and when, reduced to what its author decides:
62
+ * the instructions, where and on what it runs, and its schedule. The
63
+ * scheduler never rewrites these (it does rewrite a routine's results thread
64
+ * on each run, so that is guarded by who edits the routine instead: see the
65
+ * routines PATCH route). An edit by anyone else changes the fingerprint. */
66
+ export function routineFingerprint(routine) {
67
+ const attachments = (routine.attachments ?? []).map(attachment => [attachment.id, attachment.path]);
68
+ return createHash("sha256").update(JSON.stringify([
69
+ routine.prompt ?? "", routine.target ?? null, routine.botId ?? "", routine.groupId ?? null, attachments,
70
+ routine.schedule ?? null, routine.runOn ?? null,
71
+ ])).digest("hex");
72
+ }
73
+ const authorsFile = z.object({ version: z.literal(1), routines: z.record(z.string().max(128), z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
74
+ /** Which routines the owner wrote, as the fingerprint of what they wrote.
75
+ * Persisted beside the routines (owner-only). A missing or damaged file
76
+ * means none: it never grants anything by being absent. */
77
+ export function createCloudRoutineAuthors(file) {
78
+ let routines = {};
79
+ try {
80
+ if (existsSync(file)) {
81
+ const stat = lstatSync(file);
82
+ if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_000_000)
83
+ routines = authorsFile.parse(JSON.parse(readFileSync(file, "utf8"))).routines;
84
+ }
85
+ }
86
+ catch {
87
+ routines = {};
88
+ }
89
+ const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines }), { mode: 0o600 });
90
+ return {
91
+ /** The owner wrote this routine as it stands now. */
92
+ record(id, routine) { routines = { ...routines, [id]: routineFingerprint(routine) }; save(); },
93
+ forget(id) { if (Object.hasOwn(routines, id)) {
94
+ const next = { ...routines };
95
+ delete next[id];
96
+ routines = next;
97
+ save();
98
+ } },
99
+ /** Whether this routine, as it stands (or as a run snapshotted it), is
100
+ * exactly what the owner wrote. */
101
+ authored(id, routine) { return Object.hasOwn(routines, id) && routines[id] === routineFingerprint(routine); },
102
+ };
103
+ }