openmausbot 0.1.92 → 0.1.93

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 (104) hide show
  1. package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-CKNR3SEW.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DIYc7Ymx.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DGBaEXrM.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-CmOWzX0P.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/companion/src/routes.js +24 -1
  62. package/dist-server/container-mcp.js +6 -1
  63. package/dist-server/drivers/agents-proxy.js +2 -0
  64. package/dist-server/index.js +1722 -906
  65. package/dist-server/local-computer-proxy.js +5 -1
  66. package/dist-server/mcp-gate.js +5 -1
  67. package/dist-server/openmausbot.js +763 -93
  68. package/dist-server/pair-cli.js +763 -93
  69. package/dist-server/server/boat.js +3 -3
  70. package/dist-server/server/cloud-owner.js +1 -1
  71. package/dist-server/server/computer-control.js +6 -0
  72. package/dist-server/server/config.js +14 -1
  73. package/dist-server/server/container-computer.js +43 -32
  74. package/dist-server/server/digest.js +12 -2
  75. package/dist-server/server/drivers/acp/core.js +172 -12
  76. package/dist-server/server/drivers/acp/cursor.js +13 -1
  77. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  78. package/dist-server/server/drivers/acp/qwen.js +18 -0
  79. package/dist-server/server/drivers/builtIn.js +2 -0
  80. package/dist-server/server/drivers/cerebras.js +108 -0
  81. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  82. package/dist-server/server/drivers/claude.js +17 -3
  83. package/dist-server/server/drivers/openai-chat.js +26 -2
  84. package/dist-server/server/drivers/prompt-split.js +2 -0
  85. package/dist-server/server/env-path.js +6 -2
  86. package/dist-server/server/index.js +210 -57
  87. package/dist-server/server/local-vm-stop-reason.js +26 -0
  88. package/dist-server/server/provider-key-check.js +2 -1
  89. package/dist-server/server/routes/desktop-viewer.js +50 -6
  90. package/dist-server/server/routines.js +6 -3
  91. package/dist-server/server/store.js +54 -0
  92. package/dist-server/server/vps-computer.js +1 -1
  93. package/dist-server/server/workspace-backup.js +22 -0
  94. package/dist-server/server/workspace.js +1 -1
  95. package/dist-server/shared/approval-mode.js +1 -1
  96. package/dist-server/shared/redact.js +1 -0
  97. package/dist-server/shared/tool-name.js +22 -0
  98. package/dist-server/vps-container-mcp.js +6 -1
  99. package/dist-server/workspace-backup.worker.js +14 -0
  100. package/package.json +1 -1
  101. package/dist/assets/channel-Dtk5Mtic.js +0 -1
  102. package/dist/assets/index-Cdmjrl28.css +0 -1
  103. package/dist/assets/index-DR7YPMkW.js +0 -597
  104. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
@@ -10,6 +10,8 @@ const ROUTE = /^\/api\/desktop-viewer\/(local\/(?:shared|bot-[a-f0-9]{64}|pool-\
10
10
  const HANDSHAKE_MS = 10_000;
11
11
  const RECHECK_MS = 5_000;
12
12
  const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
13
+ const BOT_ID = /^[\w-]+$/;
14
+ const CONTROL_LEASE = /^[A-Za-z0-9_-]{16,120}$/;
13
15
  export function desktopViewerUrl(target, threadId) {
14
16
  const params = new URLSearchParams({ target, ...(threadId ? { threadId } : {}) });
15
17
  return `/desktop-viewer#${params}`;
@@ -67,7 +69,7 @@ export function createDesktopViewer(deps) {
67
69
  }).finally(() => clearTimeout(timeout));
68
70
  });
69
71
  }
70
- const route = async ({ req, res, path, method, auth, json }) => {
72
+ const route = async ({ req, res, url, path, method, auth, json }) => {
71
73
  const match = ROUTE.exec(path);
72
74
  if (!match)
73
75
  return PASS;
@@ -78,12 +80,39 @@ export function createDesktopViewer(deps) {
78
80
  // to admin, like the existing Local VM status and control endpoints.
79
81
  if (!auth.scopes.includes("admin") || !isSameOrigin(req))
80
82
  return json(res, 403, { error: "forbidden" });
83
+ // A viewer bound to a control lease: both names or neither, well formed,
84
+ // and holding before anything is inspected. Checked again with every
85
+ // liveness pass, so handing back closes the desktop within seconds. The
86
+ // conversation, when named, picks the VM seat the join picked.
87
+ const botId = url.searchParams.get("botId");
88
+ const controlLeaseId = url.searchParams.get("controlLeaseId");
89
+ const threadId = url.searchParams.get("threadId") ?? undefined;
90
+ if ((botId === null) !== (controlLeaseId === null)) {
91
+ return json(res, 400, { error: "botId and controlLeaseId go together" });
92
+ }
93
+ if (botId !== null && (!BOT_ID.test(botId) || !CONTROL_LEASE.test(controlLeaseId) || !deps.lease
94
+ || (threadId !== undefined && !BOT_ID.test(threadId)))) {
95
+ return json(res, 400, { error: "botId, threadId or controlLeaseId is not valid" });
96
+ }
97
+ if (botId === null && threadId !== undefined)
98
+ return json(res, 400, { error: "threadId needs botId and controlLeaseId" });
99
+ // Do not let a saved or constructed socket URL bypass the join refusal:
100
+ // a bot's control lease does not exclude other users of a shared desktop.
101
+ if (botId !== null && (match[1] === "local/shared" || match[1].startsWith("local/pool-"))) {
102
+ return json(res, 409, { error: "Phone control requires a per-bot Local VM. Select per-bot mode in Settings → Computers." });
103
+ }
104
+ const bound = botId === null ? undefined : deps.lease(match[1], botId, controlLeaseId, threadId);
105
+ if (botId !== null && !bound)
106
+ return json(res, 409, { error: "Take control of this computer first" });
107
+ const holds = () => bound?.() ?? true;
81
108
  const target = deps.target(match[1]);
82
109
  if (!target)
83
110
  return json(res, 404, { error: "Desktop not found" });
84
111
  const upgrade = upgrades.get(req);
85
112
  if (upgrade && auth.kind === "session")
86
113
  upgrade.owner = auth.session.id;
114
+ if (upgrade && botId !== null)
115
+ upgrade.botId = botId;
87
116
  let connection;
88
117
  try {
89
118
  connection = await target.resolve();
@@ -98,6 +127,8 @@ export function createDesktopViewer(deps) {
98
127
  const live = () => deps.live(auth) && deps.target(match[1])?.key === target.key && (connection.live?.() ?? true);
99
128
  if (!live())
100
129
  return json(res, 401, { error: "Viewer access expired" });
130
+ if (!holds())
131
+ return json(res, 409, { error: "Take control of this computer first" });
101
132
  if (!Number.isInteger(connection.port) || connection.port < 1 || connection.port > 65535) {
102
133
  return json(res, 409, { error: "The desktop viewer is not available." });
103
134
  }
@@ -150,7 +181,7 @@ export function createDesktopViewer(deps) {
150
181
  upstream.once("response", (answer) => { answer.resume(); fail(); });
151
182
  upstream.once("upgrade", (answer, remote, remoteHead) => {
152
183
  const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
153
- if (!live() || socket.destroyed || answer.headers["sec-websocket-accept"] !== accept
184
+ if (!live() || !holds() || socket.destroyed || answer.headers["sec-websocket-accept"] !== accept
154
185
  || answer.headers.upgrade?.toLowerCase() !== "websocket") {
155
186
  remote.destroy();
156
187
  fail();
@@ -171,7 +202,7 @@ export function createDesktopViewer(deps) {
171
202
  socket.pipe(remote).pipe(socket);
172
203
  connection.touch?.();
173
204
  recheck = setInterval(() => {
174
- if (!live())
205
+ if (!live() || !holds())
175
206
  upgrade.close();
176
207
  else
177
208
  connection.touch?.();
@@ -182,9 +213,22 @@ export function createDesktopViewer(deps) {
182
213
  };
183
214
  return {
184
215
  route, attach,
185
- closeForOwner: (owner) => { for (const upgrade of upgrades.values())
186
- if (upgrade.owner === owner)
187
- upgrade.close(); },
216
+ /** Close a session's viewers: all of them (sign-out, revocation), or only
217
+ * those it opened under a lease on one bot (that bot's hand-back). */
218
+ closeForOwner: (owner, botId) => {
219
+ let closed = 0;
220
+ for (const upgrade of upgrades.values()) {
221
+ if (upgrade.owner !== owner || (botId !== undefined && upgrade.botId !== botId))
222
+ continue;
223
+ // A viewer already closed stays listed until its socket's close
224
+ // event, which arrives later on Windows: not closed or counted again.
225
+ if (upgrade.socket.destroyed)
226
+ continue;
227
+ upgrade.close();
228
+ closed++;
229
+ }
230
+ return closed;
231
+ },
188
232
  closeAll: () => {
189
233
  stopped = true;
190
234
  for (const upgrade of upgrades.values())
@@ -1286,14 +1286,17 @@ export class RoutineManager {
1286
1286
  // A webhook is an incoming message, so make its task the bot's live
1287
1287
  // chat immediately. Scheduled work stays in its own task unless the
1288
1288
  // workspace has asked for runs to join the conversation they report to.
1289
+ const startedAt = this.now();
1290
+ const suffix = ` · ${new Date(startedAt).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}`;
1291
+ const title = `${run.routineName.slice(0, 80 - suffix.length).trimEnd()}${suffix}`;
1289
1292
  const joined = run.target === "room-goal" ? null : this.options.joinConversation?.(run) || null;
1290
1293
  const task = joined
1291
1294
  ? { threadId: joined }
1292
1295
  : run.target === "room-goal"
1293
1296
  ? run.groupId
1294
- ? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
1297
+ ? this.options.createGoalTask?.(run.groupId, title) ?? null
1295
1298
  : null
1296
- : this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook", run.routineId);
1299
+ : this.options.createTask(run.botId, title, run.triggerSource === "webhook", run.routineId);
1297
1300
  if (!task) {
1298
1301
  this.failRun(run, run.target === "room-goal"
1299
1302
  ? "Could not create a room task for this goal"
@@ -1301,7 +1304,7 @@ export class RoutineManager {
1301
1304
  continue;
1302
1305
  }
1303
1306
  run.threadId = task.threadId;
1304
- run.startedAt = this.now();
1307
+ run.startedAt = startedAt;
1305
1308
  run.status = "running";
1306
1309
  this.save();
1307
1310
  this.emitRun(run);
@@ -467,8 +467,13 @@ export class Store {
467
467
  mkdirSync(DATA_DIR, { recursive: true });
468
468
  for (const file of [BOTS_FILE, GROUPS_FILE])
469
469
  tightenRegistryFile(file);
470
+ // Whether the bot list is the real one. Room repair below trusts it to
471
+ // say which members no longer exist; an unreadable file must never read
472
+ // as "every member was deleted".
473
+ let botsLoaded = false;
470
474
  try {
471
475
  this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
476
+ botsLoaded = Array.isArray(this.bots);
472
477
  }
473
478
  catch {
474
479
  this.bots = [];
@@ -637,9 +642,17 @@ export class Store {
637
642
  botsMigrated = true;
638
643
  }
639
644
  }
645
+ const botIds = new Set(this.bots.map((b) => b.id));
640
646
  for (const g of this.groups) {
641
647
  g.busyBotId = null;
642
648
  delete g.turnStartedAt;
649
+ // A deleted bot used to stay a member for good: counted on the room's
650
+ // Save button and refused by the roster check on every save (MOCA-264).
651
+ // Bot-to-bot channels keep their pair; they are not edited as rooms.
652
+ if (botsLoaded && !g.dm && g.memberIds.some((id) => !botIds.has(id))) {
653
+ g.memberIds = g.memberIds.filter((id) => botIds.has(id));
654
+ groupsMigrated = true;
655
+ }
643
656
  const normalized = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, Boolean(g.dm));
644
657
  if (JSON.stringify(normalized) !== JSON.stringify(g.defaultResponder))
645
658
  groupsMigrated = true;
@@ -1764,6 +1777,27 @@ export class Store {
1764
1777
  this.saveBots(nextBots);
1765
1778
  this.bots = nextBots;
1766
1779
  this.legacyActivities.delete(id);
1780
+ // A deleted bot leaves every room it was in, and a room it led falls back
1781
+ // to its next member. Bot-to-bot channels keep their pair.
1782
+ const rooms = this.groups.filter((g) => !g.dm && g.memberIds.includes(id));
1783
+ for (const g of rooms) {
1784
+ g.memberIds = g.memberIds.filter((member) => member !== id);
1785
+ if (g.busyBotId === id) {
1786
+ g.busyBotId = null;
1787
+ delete g.turnStartedAt;
1788
+ }
1789
+ g.defaultResponder = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, false);
1790
+ }
1791
+ if (rooms.length) {
1792
+ // Bot removal is already durable. Finish erasing its data even if this
1793
+ // write fails; startup repair removes these stale memberships later.
1794
+ try {
1795
+ this.saveGroups();
1796
+ }
1797
+ catch (error) {
1798
+ console.warn("store: room cleanup will retry on restart", error);
1799
+ }
1800
+ }
1767
1801
  // every task's transcript goes with the bot, not just the open one
1768
1802
  for (const threadId of new Set([bot.threadId, ...(bot.tasks ?? []).map((t) => t.threadId)])) {
1769
1803
  this.deleteThreadRecord(threadId);
@@ -1786,6 +1820,8 @@ export class Store {
1786
1820
  // The bot folder (SOUL.md mirror) is the bot's too.
1787
1821
  removeBotFolder(id);
1788
1822
  this.emit({ type: "bot.deleted", botId: id });
1823
+ for (const g of rooms)
1824
+ this.emit({ type: "group", groupId: g.id });
1789
1825
  return true;
1790
1826
  }
1791
1827
  patchBot(id, patch) {
@@ -2435,6 +2471,24 @@ export class Store {
2435
2471
  this.emit({ type: "bot", botId });
2436
2472
  return bot;
2437
2473
  }
2474
+ /** Copy this bot's saved approval level and standing approvals onto one
2475
+ * thread. A new thread already gets them; this is how an existing
2476
+ * conversation catches up. Other threads, the transcript, and the bot
2477
+ * default stay put. A grant that has not committed yet is ignored, so a
2478
+ * refresh cannot copy the temporary Ask mask. */
2479
+ refreshTaskPermissions(botId, threadId) {
2480
+ const bot = this.bot(botId);
2481
+ const task = this.taskByThread(botId, threadId);
2482
+ if (!bot || !task)
2483
+ return null;
2484
+ const mode = approvalModeFor({ ...bot, approvalGrant: undefined });
2485
+ const alwaysAllow = structuredClone(bot.alwaysAllow ?? []);
2486
+ const autoApprove = mode === "auto";
2487
+ const sameAllow = JSON.stringify(task.alwaysAllow ?? []) === JSON.stringify(alwaysAllow);
2488
+ if (task.approvalMode === mode && task.autoApprove === autoApprove && sameAllow)
2489
+ return task;
2490
+ return this.patchTask(botId, threadId, { approvalMode: mode, autoApprove, alwaysAllow });
2491
+ }
2438
2492
  mirrorActiveTask(bot, task) {
2439
2493
  bot.threadId = task.threadId;
2440
2494
  bot.resumeCursors = structuredClone(task.resumeCursors);
@@ -817,7 +817,7 @@ export function vpsContainerRunArgs(containerName, imageRef = VPS_IMAGE, viewerS
817
817
  // someone opens the panel. unless-stopped survives reboots while still
818
818
  // honoring an explicit Stop. The shared hardening check accepts exactly
819
819
  // this policy for the VPS caller (and only "no"/unset for the Local VM,
820
- // whose desktop cannot safely resume).
820
+ // whose starts are controlled by OMB's turn lifecycle).
821
821
  "--restart",
822
822
  "unless-stopped",
823
823
  "-e",
@@ -37,6 +37,7 @@ const EXCLUSION_NOTES = [
37
37
  "Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
38
38
  "Saved credentials, provider and MCP connections, managed provider login homes and browser login profiles are not transferred. Destination connections are preserved; reconnect on a new device.",
39
39
  "Downloaded tools and caches; these can be installed again.",
40
+ "Installed dependency folders (node_modules) and symbolic links inside conversation work folders; reinstall or recreate them in the project.",
40
41
  "External project folders, CLI login homes, browser session homes, OS keychains, companion devices and other servers.",
41
42
  "VM/container disk layers and remote cloud data; durable files inside this workspace are included.",
42
43
  ];
@@ -327,6 +328,8 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
327
328
  const warnings = ["Stop external editors and managed desktops before exporting; files written outside this server cannot be frozen by the backup gate.", "Conversation text and user files are not redacted and may contain secrets you pasted. Keep the encrypted backup private.", RESTORE_WARNING];
328
329
  let bytes = 0;
329
330
  let skippedLinks = 0;
331
+ let skippedDependencies = 0;
332
+ let skippedWorkLinks = 0;
330
333
  try {
331
334
  const root = realpathSync(dataDir);
332
335
  const sourceDb = join(root, "messages.db");
@@ -370,6 +373,21 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
370
373
  throw new Error("A workspace filename cannot be safely restored on supported platforms.");
371
374
  const source = join(directory, name);
372
375
  const stat = lstatSync(source);
376
+ // Installed dependencies come back from the project's lockfile, can
377
+ // hold more files than this format allows, and are often a link an
378
+ // agent made to another checkout's install. Leave them out.
379
+ if (name === "node_modules" && (stat.isDirectory() || stat.isSymbolicLink())) {
380
+ skippedDependencies++;
381
+ continue;
382
+ }
383
+ // A conversation's work folder (task-workspaces/<bot>/<thread>) holds
384
+ // what an agent made there, and agents link things in (another
385
+ // checkout, a virtualenv). A link's target is not this workspace's
386
+ // data, so the link is left out rather than failing the backup.
387
+ if (stat.isSymbolicLink() && path.startsWith("task-workspaces/")) {
388
+ skippedWorkLinks++;
389
+ continue;
390
+ }
373
391
  if (stat.isSymbolicLink()) {
374
392
  let target;
375
393
  try {
@@ -430,6 +448,10 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
430
448
  walk(root);
431
449
  if (skippedLinks)
432
450
  warnings.push(`${skippedLinks} managed skill discovery link(s) were omitted and are recreated by the app.`);
451
+ if (skippedDependencies)
452
+ warnings.push(`${skippedDependencies} installed dependency folder(s) (node_modules) were omitted; reinstall them in the project after restoring.`);
453
+ if (skippedWorkLinks)
454
+ warnings.push(`${skippedWorkLinks} symbolic link(s) in conversation work folders were omitted; what they point to is outside this backup.`);
433
455
  const summary = {
434
456
  format: "openmaus.workspace-backup", version: 1, id: job.id, createdAt: new Date().toISOString(),
435
457
  appVersion: options.appVersion ?? "unknown", files: entries.filter((entry) => entry.type === "file").length,
@@ -20,7 +20,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
20
20
  export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
21
21
  /** MCP support does not imply native filesystem tools or a local working directory. */
22
22
  export function supportsWorkspaceFiles(driverKind) {
23
- return !["grok", "openai-compat", "minimax", "mistral", "boxAgent"].includes(driverKind);
23
+ return !["grok", "openai-compat", "minimax", "mistral", "cerebras", "boxAgent"].includes(driverKind);
24
24
  }
25
25
  /** Default task files are private to the thread, outside the bot's shared
26
26
  * memory folder. This is directory organization, not a shell sandbox. */
@@ -21,7 +21,7 @@ export function supportsApprovalMode(driverKind, mode) {
21
21
  // engines could never stop asking — not by its own level, and not through
22
22
  // a Chief's delegated Full access either.
23
23
  return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo", "qwenAgent", "geminiAgent",
24
- "openai-compat", "grok", "minimax", "mistral"].includes(driverKind ?? "");
24
+ "openai-compat", "grok", "minimax", "mistral", "cerebras"].includes(driverKind ?? "");
25
25
  }
26
26
  /** A Full/Custom grant belongs to one provider's tool semantics. Other
27
27
  * modes carry across only when the destination actually implements them.
@@ -21,6 +21,7 @@ const KEY_PREFIXES = [
21
21
  /\bsk-(?:ant-|proj-|live-|test-)?[A-Za-z0-9_-]{16,}/g, // anthropic / openai / stripe
22
22
  /\bxai-[A-Za-z0-9_-]{20,}/g, // xai (grok)
23
23
  /\bgsk_[A-Za-z0-9]{40,}/g, // groq
24
+ /\bcsk-[A-Za-z0-9]{32,}/g, // cerebras
24
25
  /\bhf_[A-Za-z0-9]{30,}/g, // hugging face
25
26
  /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g, // github classic
26
27
  /\bgithub_pat_[A-Za-z0-9_]{20,}/g, // github fine-grained
@@ -0,0 +1,22 @@
1
+ // A tool chip's display title is not a tool identity. Codex titles a
2
+ // commandExecution chip with the whole command line (drivers/codex.ts) and
3
+ // the ACP core prefers `rawInput.command` over the call's own name
4
+ // (drivers/acp/core.ts), because in a transcript the command IS the useful
5
+ // label. Anything that counts or groups tool calls needs the identity
6
+ // instead, so both readers share one predicate rather than guessing twice.
7
+ /** The generic identity every command-titled chip counts under. */
8
+ export const SHELL_TOOL = "shell";
9
+ /** A tool name that is itself a command line rather than a bare tool name
10
+ * such as `Bash`. Whitespace or a slash: no provider names a tool that way,
11
+ * and every shell command has one by the time it carries an argument or a
12
+ * path. */
13
+ export function nameIsCommand(name) {
14
+ return /[\s/]/.test(name);
15
+ }
16
+ /** What a tool chip should be counted as. Command-titled chips collapse to
17
+ * one `shell` bucket: keyed on the raw command they never aggregate (a turn
18
+ * that ran one tool twelve times reads as twelve tools), and the raw title
19
+ * is unredacted — unlike `tool.summary`, which drivers bound and redact. */
20
+ export function toolIdentity(name) {
21
+ return nameIsCommand(name) ? SHELL_TOOL : name;
22
+ }
@@ -136,6 +136,8 @@ function windowsKnownDirs() {
136
136
  // x.ai installer
137
137
  join(localAppData, "agy", "bin"),
138
138
  // Antigravity installer
139
+ // Cursor installer: cursor-agent.* and its `agent` copies (MOCA-272)
140
+ join(localAppData, "cursor-agent"),
139
141
  join(home, ".local", "bin"),
140
142
  // claude native installer
141
143
  join(home, ".claude", "local"),
@@ -143,7 +145,9 @@ function windowsKnownDirs() {
143
145
  // Factory droid installer (%USERPROFILE%\bin)
144
146
  join(home, ".bun", "bin"),
145
147
  join(home, ".deno", "bin"),
146
- join(home, "go", "bin")
148
+ join(home, "go", "bin"),
149
+ join(process.env.ProgramFiles ?? "C:\\Program Files", "Docker", "Docker", "resources", "bin")
150
+ // Docker Desktop installer
147
151
  ];
148
152
  }
149
153
  var cached = null;
@@ -16515,6 +16519,7 @@ var appConfigSchema = external_exports.object({
16515
16519
  phone: external_exports.enum(["ios", "android"]).optional()
16516
16520
  }).optional(),
16517
16521
  mistral: external_exports.object({ key: optionalText }).optional(),
16522
+ cerebras: external_exports.object({ key: optionalText }).optional(),
16518
16523
  xai: external_exports.object({ key: optionalText, url: optionalText }).optional(),
16519
16524
  /** Anthropic API key for Claude Code billed per token, handed only to
16520
16525
  * Claude instances; `url` only for a proxy or a test double. Never a
@@ -19078,6 +19078,7 @@ var appConfigSchema = external_exports.object({
19078
19078
  phone: external_exports.enum(["ios", "android"]).optional()
19079
19079
  }).optional(),
19080
19080
  mistral: external_exports.object({ key: optionalText }).optional(),
19081
+ cerebras: external_exports.object({ key: optionalText }).optional(),
19081
19082
  xai: external_exports.object({ key: optionalText, url: optionalText }).optional(),
19082
19083
  /** Anthropic API key for Claude Code billed per token, handed only to
19083
19084
  * Claude instances; `url` only for a proxy or a test double. Never a
@@ -19321,6 +19322,7 @@ var EXCLUSION_NOTES = [
19321
19322
  "Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
19322
19323
  "Saved credentials, provider and MCP connections, managed provider login homes and browser login profiles are not transferred. Destination connections are preserved; reconnect on a new device.",
19323
19324
  "Downloaded tools and caches; these can be installed again.",
19325
+ "Installed dependency folders (node_modules) and symbolic links inside conversation work folders; reinstall or recreate them in the project.",
19324
19326
  "External project folders, CLI login homes, browser session homes, OS keychains, companion devices and other servers.",
19325
19327
  "VM/container disk layers and remote cloud data; durable files inside this workspace are included."
19326
19328
  ];
@@ -19515,6 +19517,8 @@ async function createWorkspaceBackupSnapshot(dataDir2, options2, jobId2) {
19515
19517
  const warnings = ["Stop external editors and managed desktops before exporting; files written outside this server cannot be frozen by the backup gate.", "Conversation text and user files are not redacted and may contain secrets you pasted. Keep the encrypted backup private.", RESTORE_WARNING];
19516
19518
  let bytes = 0;
19517
19519
  let skippedLinks = 0;
19520
+ let skippedDependencies = 0;
19521
+ let skippedWorkLinks = 0;
19518
19522
  try {
19519
19523
  const root = realpathSync(dataDir2);
19520
19524
  const sourceDb = join3(root, "messages.db");
@@ -19545,6 +19549,14 @@ async function createWorkspaceBackupSnapshot(dataDir2, options2, jobId2) {
19545
19549
  if (!validRelative(path2)) throw new Error("A workspace filename cannot be safely restored on supported platforms.");
19546
19550
  const source = join3(directory, name);
19547
19551
  const stat = lstatSync(source);
19552
+ if (name === "node_modules" && (stat.isDirectory() || stat.isSymbolicLink())) {
19553
+ skippedDependencies++;
19554
+ continue;
19555
+ }
19556
+ if (stat.isSymbolicLink() && path2.startsWith("task-workspaces/")) {
19557
+ skippedWorkLinks++;
19558
+ continue;
19559
+ }
19548
19560
  if (stat.isSymbolicLink()) {
19549
19561
  let target;
19550
19562
  try {
@@ -19590,6 +19602,8 @@ async function createWorkspaceBackupSnapshot(dataDir2, options2, jobId2) {
19590
19602
  };
19591
19603
  walk(root);
19592
19604
  if (skippedLinks) warnings.push(`${skippedLinks} managed skill discovery link(s) were omitted and are recreated by the app.`);
19605
+ if (skippedDependencies) warnings.push(`${skippedDependencies} installed dependency folder(s) (node_modules) were omitted; reinstall them in the project after restoring.`);
19606
+ if (skippedWorkLinks) warnings.push(`${skippedWorkLinks} symbolic link(s) in conversation work folders were omitted; what they point to is outside this backup.`);
19593
19607
  const summary = {
19594
19608
  format: "openmaus.workspace-backup",
19595
19609
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmausbot",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "description": "Run the OpenMausBot server anywhere and pair your devices to it",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-CF1zwoQ-.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};