openmausbot 0.1.96 → 0.1.98

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 (185) hide show
  1. package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
  2. package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
  3. package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
  4. package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
  5. package/dist/assets/DesktopViewer-utONms1p.js +5 -0
  6. package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
  7. package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
  8. package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
  9. package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
  10. package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
  11. package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
  12. package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
  13. package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
  14. package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
  15. package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
  16. package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
  17. package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
  18. package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
  19. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
  20. package/dist/assets/activity-BxOkayvD.js +1 -0
  21. package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
  22. package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
  23. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
  24. package/dist/assets/brain-CFZ7PEXc.js +1 -0
  25. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
  26. package/dist/assets/channel-WhFyujhZ.js +1 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
  28. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
  29. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
  30. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
  31. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
  32. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
  33. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
  34. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
  35. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
  36. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
  37. package/dist/assets/computer-control-NLov352p.js +1 -0
  38. package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
  39. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
  40. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
  41. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
  42. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
  43. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
  44. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
  45. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
  46. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
  47. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
  49. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
  50. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
  51. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
  52. package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
  53. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
  54. package/dist/assets/image-plus-BbVjGHin.js +1 -0
  55. package/dist/assets/index-BrPW5HDO.css +1 -0
  56. package/dist/assets/index-C3NqfdVU.js +585 -0
  57. package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
  58. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
  60. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
  61. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
  62. package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
  63. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
  64. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
  65. package/dist/assets/module-DCUsy2BQ.js +3 -0
  66. package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
  67. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
  68. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
  70. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
  71. package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
  72. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
  73. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
  74. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
  75. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
  76. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
  77. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
  78. package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
  79. package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
  80. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
  81. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
  82. package/dist/assets/user-DcZBnAuv.js +1 -0
  83. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
  84. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
  85. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
  86. package/dist/index.html +2 -2
  87. package/dist-server/cloud-home-start.js +20 -9
  88. package/dist-server/container-mcp.js +8 -0
  89. package/dist-server/desktop-entry.mjs +54 -0
  90. package/dist-server/drivers/agents-proxy.js +26 -9
  91. package/dist-server/index.js +3734 -2360
  92. package/dist-server/openmausbot.js +1664 -1553
  93. package/dist-server/pair-cli.js +1664 -1553
  94. package/dist-server/permission-proxy.js +2 -4
  95. package/dist-server/server/atomic.js +70 -2
  96. package/dist-server/server/auto-approve.js +1 -1
  97. package/dist-server/server/auto-vm-claims.js +13 -1
  98. package/dist-server/server/automatic-recovery.js +1 -2
  99. package/dist-server/server/boat.js +68 -84
  100. package/dist-server/server/bot-overview.js +1 -1
  101. package/dist-server/server/browser-engine.js +38 -1
  102. package/dist-server/server/browser-live.js +112 -25
  103. package/dist-server/server/browser-runtime.js +31 -1
  104. package/dist-server/server/checkpoints.js +275 -44
  105. package/dist-server/server/cli-setup.js +3 -3
  106. package/dist-server/server/cloud-computer-tools.js +11 -4
  107. package/dist-server/server/cloud-home-start.js +4 -4
  108. package/dist-server/server/cloud-home.js +3 -9
  109. package/dist-server/server/cloud-owner.js +1 -1
  110. package/dist-server/server/computer-engine-removal.js +70 -0
  111. package/dist-server/server/computer-selection.js +18 -0
  112. package/dist-server/server/config.js +29 -12
  113. package/dist-server/server/connector-card-text.js +15 -0
  114. package/dist-server/server/connector-scope-hint.js +83 -0
  115. package/dist-server/server/default-model-selection.js +23 -14
  116. package/dist-server/server/delegations.js +11 -5
  117. package/dist-server/server/delta-context.js +6 -1
  118. package/dist-server/server/drivers/acp/core.js +11 -1
  119. package/dist-server/server/drivers/acp/grok.js +47 -5
  120. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  121. package/dist-server/server/drivers/agents-call.js +1 -1
  122. package/dist-server/server/drivers/agents-catalog.js +27 -7
  123. package/dist-server/server/drivers/agents-proxy.js +1 -0
  124. package/dist-server/server/drivers/builtIn.js +0 -2
  125. package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
  126. package/dist-server/server/drivers/claude-login-auth.js +1 -1
  127. package/dist-server/server/drivers/claude.js +84 -41
  128. package/dist-server/server/drivers/codex-device-auth.js +27 -319
  129. package/dist-server/server/drivers/codex.js +2 -2
  130. package/dist-server/server/drivers/device-auth.js +342 -0
  131. package/dist-server/server/drivers/openai-chat.js +110 -10
  132. package/dist-server/server/drivers/pi.js +184 -49
  133. package/dist-server/server/fleet-agent.js +18 -5
  134. package/dist-server/server/harness/http.js +5 -2
  135. package/dist-server/server/harness/registry.js +19 -0
  136. package/dist-server/server/harness-capabilities.js +7 -4
  137. package/dist-server/server/hosted-models.js +3 -1
  138. package/dist-server/server/index.js +823 -333
  139. package/dist-server/server/key-rejections.js +57 -0
  140. package/dist-server/server/live-call.js +11 -6
  141. package/dist-server/server/managed-policy.js +1 -1
  142. package/dist-server/server/message-file.js +7 -4
  143. package/dist-server/server/model-catalog/presets.js +8 -0
  144. package/dist-server/server/peer-roster.js +10 -2
  145. package/dist-server/server/permission-proxy.js +4 -0
  146. package/dist-server/server/provider-key-check.js +5 -1
  147. package/dist-server/server/request-auth.js +1 -1
  148. package/dist-server/server/room-turn-end.js +15 -0
  149. package/dist-server/server/room-turn-timeout.js +28 -0
  150. package/dist-server/server/routes/live.js +3 -3
  151. package/dist-server/server/routes/thread-models.js +36 -0
  152. package/dist-server/server/routine-requests.js +1 -1
  153. package/dist-server/server/routines.js +3 -3
  154. package/dist-server/server/screen-frame-gate.js +1 -3
  155. package/dist-server/server/skills.js +3 -2
  156. package/dist-server/server/store.js +273 -61
  157. package/dist-server/server/surface.js +29 -49
  158. package/dist-server/server/system-prompt.js +10 -10
  159. package/dist-server/server/team-backup.js +19 -4
  160. package/dist-server/server/thread-model.js +25 -0
  161. package/dist-server/server/turn-continuation.js +168 -0
  162. package/dist-server/server/webhook-ingress.js +8 -3
  163. package/dist-server/server/webhooks.js +40 -5
  164. package/dist-server/server/workspace.js +1 -1
  165. package/dist-server/shared/ask-question.js +0 -70
  166. package/dist-server/shared/cloud-computer.js +4 -6
  167. package/dist-server/shared/credential-request.js +1 -1
  168. package/dist-server/shared/device-sign-in.js +57 -0
  169. package/dist-server/shared/failed-turn.js +1 -4
  170. package/dist-server/shared/place-view.js +409 -0
  171. package/dist-server/shared/team-backup.js +2 -0
  172. package/dist-server/shared/thread-model.js +13 -0
  173. package/dist-server/shared/tool-scope-support.js +1 -2
  174. package/dist-server/vps-container-mcp.js +8 -0
  175. package/dist-server/workspace-backup.worker.js +41 -22
  176. package/package.json +1 -1
  177. package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
  178. package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
  179. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  180. package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
  181. package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
  182. package/dist/assets/index-Du7o6Dsf.js +0 -610
  183. package/dist/assets/index-KxG4bWSr.css +0 -1
  184. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  185. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -0,0 +1,57 @@
1
+ // A saved key counts as working until the provider says otherwise. When a
2
+ // real use of it is refused as a bad key (a chat request, or Test in
3
+ // Settings → API keys), every engine on that key and endpoint reports it as
4
+ // not signed in, until the key is saved or cleared again or a later use of
5
+ // it succeeds. In memory only: after a restart the key shows as working
6
+ // until its next use fails. The key itself is never kept, only a hash of it
7
+ // beside the provider's base URL.
8
+ import { createHash } from "node:crypto";
9
+ export const KEY_REJECTED_REASON = "The provider rejected this key. Change it in Settings → API keys.";
10
+ const rejected = new Set();
11
+ const listeners = new Set();
12
+ const fingerprint = (key) => createHash("sha256").update(key.trim()).digest("hex").slice(0, 32);
13
+ const markOf = (url, key) => `${url.trim().replace(/\/+$/, "")} ${fingerprint(key)}`;
14
+ const changed = () => { for (const listener of listeners)
15
+ listener(); };
16
+ export function keyRejected(url, key) {
17
+ return rejected.size > 0 && rejected.has(markOf(url, key));
18
+ }
19
+ export function noteKeyRejected(url, key) {
20
+ const mark = markOf(url, key);
21
+ if (rejected.has(mark))
22
+ return;
23
+ rejected.add(mark);
24
+ changed();
25
+ }
26
+ export function noteKeyAccepted(url, key) {
27
+ if (rejected.size > 0 && rejected.delete(markOf(url, key)))
28
+ changed();
29
+ }
30
+ /** Drops every mark on this key, whatever endpoint it was used against. */
31
+ export function forgetKey(key) {
32
+ if (rejected.size === 0)
33
+ return;
34
+ const suffix = ` ${fingerprint(key)}`;
35
+ const before = rejected.size;
36
+ for (const mark of rejected)
37
+ if (mark.endsWith(suffix))
38
+ rejected.delete(mark);
39
+ if (rejected.size !== before)
40
+ changed();
41
+ }
42
+ export function onKeyRejectionChange(listener) {
43
+ listeners.add(listener);
44
+ return () => listeners.delete(listener);
45
+ }
46
+ /** The provider refused the key itself: HTTP 401, or a 400/403 whose body
47
+ * says the key is invalid or revoked (xAI answers a wrong key with a 400).
48
+ * Rate limits, quota, billing and model access are not the key's fault. */
49
+ export function rejectsKey(status, body) {
50
+ if (/\b(?:quota|billing|credits?|rate.?limit|insufficient.?(?:balance|funds))\b/i.test(body))
51
+ return false;
52
+ if (status === 401)
53
+ return true;
54
+ if (status !== 400 && status !== 403)
55
+ return false;
56
+ return /\binvalid_api_key\b|\b(?:invalid|incorrect|revoked|expired|disabled|deactivated)[ _-]?(?:api[ _-]?)?key\b|\bapi[ _-]?key (?:is |was |has been )?(?:invalid|incorrect|revoked|expired|disabled|not valid)\b/i.test(body);
57
+ }
@@ -11,6 +11,7 @@
11
11
  // This file owns the one server-side step: exchanging the renderer's WebRTC
12
12
  // offer for an answer. The OpenAI key never leaves the harness; the renderer
13
13
  // only ever holds the SDP answer and the media connection it describes.
14
+ import { CLOUD_HOME_PLACE } from "./system-prompt.js";
14
15
  export const LIVE_MODEL = "gpt-live-1";
15
16
  const OPENAI_BASE = "https://api.openai.com";
16
17
  export const DEFAULT_LIVE_VOICE = "marin";
@@ -57,14 +58,18 @@ export class LiveSessionError extends Error {
57
58
  }
58
59
  }
59
60
  /** Frontend instructions for the voice. Business rules stay with the bot;
60
- * this only says who is speaking and when to hand work over. Structure
61
- * follows the GPT-Live prompting guide's template. */
62
- export function liveInstructions(bot) {
61
+ * this only says who is speaking, where it runs and when to hand work over.
62
+ * Structure follows the GPT-Live prompting guide's template. `cloudHome`:
63
+ * the harness is the person's My Cloud, named as the bot's own system prompt
64
+ * names it (CLOUD_HOME_PLACE). */
65
+ export function liveInstructions(bot, { cloudHome = false } = {}) {
63
66
  const name = oneLine(bot.name) || "the agent";
64
67
  const title = oneLine(bot.title ?? "");
65
68
  const description = oneLine(bot.description ?? "").slice(0, 400);
69
+ const where = cloudHome ? `runs on ${CLOUD_HOME_PLACE}` : "runs in OpenMausBot on the user's own computer";
70
+ const changes = cloudHome ? "My Cloud" : "the computer";
66
71
  return [
67
- `You are ${name}${title ? `, ${title}` : ""}, an AI agent that runs in OpenMausBot on the user's own computer.${description ? ` ${description}` : ""}`,
72
+ `You are ${name}${title ? `, ${title}` : ""}, an AI agent that ${where}.${description ? ` ${description}` : ""}`,
68
73
  `To the user you are one assistant, ${name}, and you speak in the first person. Your work — looking things up, using your tools, files and memory, researching, deciding, and answering anything that needs facts this conversation does not hold — happens when you delegate. Delegating is how you think and act; it is not someone else.`,
69
74
  "Never mention a backend, delegation, a voice layer, or another system or model doing the work, and never say you are only a voice. Say \"I\" about the work.",
70
75
  "Never ask the user whether you may look something up or check something. When it needs checking, delegate at once and say briefly that you are checking.",
@@ -74,7 +79,7 @@ export function liveInstructions(bot) {
74
79
  "Interruption policy: Stop speaking when the user interrupts. Listen to what they say.",
75
80
  "Delegation policy:",
76
81
  "Backend tools:",
77
- `- ${name}: your own files, tools, memory, settings and this conversation's history. It researches, writes, changes things on the computer, and answers questions, including questions about yourself such as which AI model you run on, your settings, your tools and your memory.`,
82
+ `- ${name}: your own files, tools, memory, settings and this conversation's history. It researches, writes, changes things on ${changes}, and answers questions, including questions about yourself such as which AI model you run on, your settings, your tools and your memory.`,
78
83
  "Delegate to the backend when:",
79
84
  "- The user asks a question, asks for work, or gives an instruction.",
80
85
  "- The user asks something about you that this conversation does not already answer, for example which AI model you run on.",
@@ -131,7 +136,7 @@ export async function createLiveSession(input) {
131
136
  const body = {
132
137
  session: {
133
138
  model: LIVE_MODEL,
134
- instructions: liveInstructions(input.bot),
139
+ instructions: liveInstructions(input.bot, { cloudHome: input.cloudHome }),
135
140
  // client delegation: every request comes back to the harness, which
136
141
  // runs it as a normal turn on the bot
137
142
  delegation: { type: "client" },
@@ -16,7 +16,7 @@ const policySchema = z.object({
16
16
  computers: z.object({ thisComputer: z.boolean(), localVm: z.boolean(), box: z.boolean(), vps: z.boolean() }).strict(),
17
17
  remoteAccess: z.boolean(),
18
18
  }).strict();
19
- const computerLabels = { thisComputer: "this computer", localVm: "local virtual machines", box: "Boat cloud computers", vps: "VPS computers" };
19
+ const computerLabels = { thisComputer: "this computer", localVm: "local virtual machines", box: "cloud computers", vps: "VPS computers" };
20
20
  export function parseManagedPolicy(raw, now = Date.now()) {
21
21
  if (raw === null)
22
22
  return null;
@@ -17,9 +17,12 @@ export function messageFileRoots(options) {
17
17
  options.attachments,
18
18
  ])];
19
19
  }
20
- function statusError(status, message) {
21
- return Object.assign(new Error(message), { status });
20
+ function statusError(status, message, code) {
21
+ return Object.assign(new Error(message), { status, ...(code ? { code } : {}) });
22
22
  }
23
+ // The client offers "Show in folder" for exactly this refusal, so it carries a
24
+ // stable code instead of being recognised by its English text.
25
+ const outsideWorkspace = () => statusError(403, "the linked file is outside this conversation's workspace", "outside_workspace");
23
26
  function decodePathWithSuffixRemoved(href) {
24
27
  // Split before decoding so an encoded `?` or `#` remains part of the
25
28
  // filename, while a real Markdown query/fragment never reaches the file
@@ -396,7 +399,7 @@ export async function openMessageFile(href, roots) {
396
399
  }
397
400
  const canonicalAfter = await realpath(candidate);
398
401
  if (!containedBy(root, canonicalAfter))
399
- throw statusError(403, "the linked file is outside this conversation's workspace");
402
+ throw outsideWorkspace();
400
403
  const after = await stat(canonicalAfter);
401
404
  if (opened.dev !== after.dev || opened.ino !== after.ino) {
402
405
  throw statusError(409, "the linked file changed while it was being opened");
@@ -416,7 +419,7 @@ export async function openMessageFile(href, roots) {
416
419
  }
417
420
  }
418
421
  if (sawOutsideRoot)
419
- throw statusError(403, "the linked file is outside this conversation's workspace");
422
+ throw outsideWorkspace();
420
423
  throw statusError(404, "the linked file is unavailable");
421
424
  }
422
425
  /** A safe attachment header with a readable ASCII fallback and UTF-8 name. */
@@ -139,6 +139,14 @@ export const PROVIDER_PRESETS = [
139
139
  anthropic: { baseUrl: "https://opencode.ai/zen" },
140
140
  checkedAt: CHECKED,
141
141
  },
142
+ {
143
+ id: "wallaby",
144
+ catalogId: "wallaby",
145
+ label: "Wallaby",
146
+ api: "https://api.wallabytoken.com/v1",
147
+ keyCheck: "/models",
148
+ checkedAt: "2026-10-05",
149
+ },
142
150
  ];
143
151
  const BY_ID = new Map(PROVIDER_PRESETS.map((preset) => [preset.id, preset]));
144
152
  export function providerPreset(id) {
@@ -6,10 +6,18 @@
6
6
  import { sameAudience } from "./bot-visibility.js";
7
7
  const sectionKey = (section) => section?.trim() || "";
8
8
  export const PEER_ACCESS_HELP = "Call list_bots for reachable teammates. If the intended Chief is missing, ask the user to check team membership and this bot's allowed peers, or message the Chief directly. A Chief's access to another team does not grant that team's bots access back to the Chief. Do not use computer control to bypass this.";
9
+ /** OMB_OPEN_TEAMS=1 runs the workspace as one team: any bot may reach a bot
10
+ * in any section, so sections only group the sidebar. Off by default. Peer
11
+ * lists, hidden bots and visibility still apply, and it grants no Chief
12
+ * authority (coordinatorSupervises). Read per call, so no caller caches it. */
13
+ const openTeams = () => process.env.OMB_OPEN_TEAMS === "1";
9
14
  /** Coordination is scoped to the bot's own team unless the owner explicitly
10
- * allows its Chief to work with additional teams. A title, peer id, imported
11
- * persona or a room membership is not a grant. Invalid saved grants fail closed. */
15
+ * allows its Chief to work with additional teams, or opens every team to
16
+ * every bot (openTeams). A title, peer id, imported persona or a room
17
+ * membership is not a grant. Invalid saved grants fail closed. */
12
18
  export function canAccessTeam(from, section) {
19
+ if (openTeams())
20
+ return true;
13
21
  const target = sectionKey(section);
14
22
  return target === sectionKey(from.section) || Boolean(from.chiefOfStaff &&
15
23
  Array.isArray(from.managedSections) && from.managedSections.some(value => typeof value === "string" && sectionKey(value) === target));
@@ -36,6 +36,9 @@ const allowQuestion = allowsTool(selection.scope, { kind: "mcp", server: "ogb",
36
36
  const socketPath = process.argv[2] ?? "";
37
37
  const waiting = new Map();
38
38
  const conn = connect(socketPath);
39
+ // Both streams below carry JSON lines that can be larger than one read.
40
+ // Decoding as a stream keeps a character split across two reads whole.
41
+ conn.setEncoding("utf8");
39
42
  const dead = () => {
40
43
  for (const resolve of waiting.values()) {
41
44
  resolve({ behavior: "deny", message: "OpenMausBot: permission broker unavailable — skip this action" });
@@ -227,6 +230,7 @@ async function handle(msg) {
227
230
  }
228
231
  }
229
232
  let inBuf = "";
233
+ process.stdin.setEncoding("utf8");
230
234
  process.stdin.on("data", (chunk) => {
231
235
  inBuf += chunk;
232
236
  let nl;
@@ -14,9 +14,13 @@ export function isLoopback(hostname) {
14
14
  const host = hostname.replace(/^\[|\]$/g, "");
15
15
  return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
16
16
  }
17
+ /** A provider's base URL: the configured one, or its default. */
18
+ export function providerBaseUrl(provider, base) {
19
+ return (base?.trim() || DEFAULT_URLS[provider]).replace(/\/+$/, "");
20
+ }
17
21
  /** The models endpoint for a provider, from its base URL or the default. */
18
22
  export function providerModelsUrl(provider, base) {
19
- const root = (base?.trim() || DEFAULT_URLS[provider]).replace(/\/+$/, "");
23
+ const root = providerBaseUrl(provider, base);
20
24
  if (provider === "anthropic")
21
25
  return root.endsWith("/v1") ? `${root}/models` : `${root}/v1/models`;
22
26
  return `${root}/models`;
@@ -340,7 +340,7 @@ export function clientBotPatchViolation(body) {
340
340
  return null;
341
341
  }
342
342
  /** Same for a room: name and reading state, never its folder or who answers. */
343
- const CLIENT_GROUP_PATCH_FIELDS = new Set(["name", "bulletin", "unread", "pinnedMessageId", "section"]);
343
+ const CLIENT_GROUP_PATCH_FIELDS = new Set(["name", "bulletin", "unread", "pinnedMessageId", "section", "turnTimeoutMinutes"]);
344
344
  export function clientGroupPatchViolation(body) {
345
345
  if (!body || typeof body !== "object" || Array.isArray(body))
346
346
  return "body";
@@ -0,0 +1,15 @@
1
+ /** How a room member's provider turn ended, read from its turn.completed and
2
+ * from what ended it early: the member's cloud computer, claimed by its first
3
+ * computer call, could not start (a failure with its cause) or parked waiting
4
+ * for its seat. Kept free of index.ts so the rule stays unit-testable. */
5
+ export function roomTurnEnd(event, orchestrated, claim) {
6
+ if (claim?.parked)
7
+ return { outcome: "parked" };
8
+ // The cause, not the "interrupted" the ending interrupt settles as; and a
9
+ // chat round stops here, as it stops on a failure at setup.
10
+ if (claim)
11
+ return { outcome: "provider_failed", stopReason: claim.message };
12
+ if (orchestrated && !event.ok)
13
+ return { outcome: "provider_failed", stopReason: event.stopReason ?? null };
14
+ return { outcome: "settled" };
15
+ }
@@ -1,6 +1,34 @@
1
+ import { MAX_ROOM_TURN_TIMEOUT_MINUTES, MIN_ROOM_TURN_TIMEOUT_MINUTES, } from "./config.js";
1
2
  export function roomTurnTimeoutMs(minutes) {
2
3
  return minutes * 60_000;
3
4
  }
5
+ /** A conversation may replace the global group ceiling. Null clears that
6
+ * choice. Anything else is refused rather than silently clamped. */
7
+ export function parseConversationTurnTimeout(value) {
8
+ if (value === null)
9
+ return { ok: true, minutes: null };
10
+ if (typeof value !== "number" ||
11
+ !Number.isInteger(value) ||
12
+ value < MIN_ROOM_TURN_TIMEOUT_MINUTES ||
13
+ value > MAX_ROOM_TURN_TIMEOUT_MINUTES) {
14
+ return {
15
+ ok: false,
16
+ error: "Turn limit must be a whole number from 1 to 1,440, or null to use the group default.",
17
+ };
18
+ }
19
+ return { ok: true, minutes: value };
20
+ }
21
+ /** The ceiling captured for one turn. An override wins; a missing or
22
+ * unusable one keeps the global group limit. */
23
+ export function effectiveRoomTurnTimeoutMinutes(globalMinutes, override) {
24
+ if (typeof override === "number" &&
25
+ Number.isInteger(override) &&
26
+ override >= MIN_ROOM_TURN_TIMEOUT_MINUTES &&
27
+ override <= MAX_ROOM_TURN_TIMEOUT_MINUTES) {
28
+ return override;
29
+ }
30
+ return globalMinutes;
31
+ }
4
32
  /**
5
33
  * One room turn's slice of the configured room ceiling.
6
34
  *
@@ -8,11 +8,11 @@ const sessionBody = z.object({
8
8
  threadId: id.optional(),
9
9
  // Not trimmed: the SDP offer goes to OpenAI byte for byte.
10
10
  sdp: z.string().min(1).refine((sdp) => Buffer.byteLength(sdp) <= MAX_SDP_BYTES),
11
- client: z.enum(["desktop", "ios", "android"]),
11
+ client: z.enum(["desktop", "web", "ios", "android"]),
12
12
  }).strict();
13
13
  const endBody = z.object({ callId: z.string().min(1).max(120) }).strict();
14
14
  // Strict, so `key` (or anything else) is refused rather than ignored: the key
15
- // is only ever set on the computer that runs the harness.
15
+ // is saved through the desktop's credential store or PUT /api/config, never here.
16
16
  const settingsBody = z.object({
17
17
  voice: z.string().trim().max(40).regex(/^[a-z]*$/).optional(),
18
18
  readTypedReplies: z.boolean().optional(),
@@ -86,7 +86,7 @@ export function createLiveRoutes(deps) {
86
86
  if (method === "PATCH" && path === "/api/live/settings") {
87
87
  const parsed = settingsBody.safeParse(await bodyOf(req, readBody));
88
88
  if (!parsed.success) {
89
- return json(res, 400, { error: "Those Live settings are not valid. The OpenAI key can only be changed on the computer that runs OpenMausBot." });
89
+ return json(res, 400, { error: "Those Live settings are not valid." });
90
90
  }
91
91
  // as PUT /api/config: an empty patch is not saved (or broadcast)
92
92
  if (!Object.keys(parsed.data).length)
@@ -0,0 +1,36 @@
1
+ import { sameModelSelection } from "../../shared/thread-model.js";
2
+ import { PASS } from "./table.js";
3
+ const MAX_THREAD_IDS = 10_000;
4
+ export function createThreadModelRoutes(deps) {
5
+ return async ({ req, res, path, method, auth, json, readBody }) => {
6
+ const m = path.match(/^\/api\/bots\/([\w-]+)\/threads\/follow-model$/);
7
+ if (!m || method !== "POST")
8
+ return PASS;
9
+ const bot = deps.bot(m[1]);
10
+ if (!bot)
11
+ return json(res, 404, { error: "no such bot" });
12
+ if (!deps.mayChangeModel(auth))
13
+ return json(res, 403, { error: "On this Cloud only its owner can change a bot's model." });
14
+ const body = await readBody(req);
15
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key) => key !== "threadIds")) {
16
+ return json(res, 400, { error: "send {} or { threadIds }" });
17
+ }
18
+ const raw = body.threadIds;
19
+ if (raw !== undefined && (!Array.isArray(raw) || raw.length > MAX_THREAD_IDS || raw.some((id) => typeof id !== "string"))) {
20
+ return json(res, 400, { error: "threadIds must be a list of this bot's thread ids" });
21
+ }
22
+ const tasks = bot.tasks ?? [];
23
+ const ids = new Set(tasks.map((task) => task.threadId));
24
+ const foreign = raw?.find((id) => !ids.has(id));
25
+ if (foreign !== undefined)
26
+ return json(res, 400, { error: `"${foreign}" is not one of this bot's threads` });
27
+ if (bot.approvalGrant)
28
+ return json(res, 409, { error: "wait for the approval-level change to finish before changing models" });
29
+ const listed = raw === undefined ? null : new Set(raw);
30
+ const own = tasks.filter((task) => (!listed || listed.has(task.threadId)) &&
31
+ task.modelSelection !== undefined && !sameModelSelection(task.modelSelection, bot.modelSelection));
32
+ const busy = own.filter((task) => deps.busy(bot.id, task.threadId));
33
+ const switched = deps.follow(bot.id, own.filter((task) => !busy.includes(task)).map((task) => task.threadId));
34
+ return json(res, 200, { switched: switched.length, busy: busy.length, bot: deps.reply(bot.id) });
35
+ };
36
+ }
@@ -674,7 +674,7 @@ function cardCopy(operation, manager, timeZone, now) {
674
674
  const nextRunAt = nextForOperation(operation, manager, now);
675
675
  const scheduleTimeZone = definition.schedule.type === "cron" ? definition.schedule.timeZone : timeZone;
676
676
  const when = operation.action === "run_now" ? "Now" : scheduleText(definition.schedule, timeZone);
677
- const destination = definition.runOn === "cloud" ? "Bot’s current model on its Boat cloud computer" : "Bot’s current model and configured computer";
677
+ const destination = definition.runOn === "cloud" ? "Bot’s current model on its cloud computer" : "Bot’s current model and configured computer";
678
678
  const current = operation.action === "create"
679
679
  ? null
680
680
  : manager.listRoutines().find((routine) => routine.id === operation.routineId) ?? null;
@@ -943,7 +943,7 @@ export class RoutineManager {
943
943
  this.commitMutation(() => {
944
944
  run = this.newRun(routine, this.now(), true, allocations, request?.threadId ?? routine.sourceThreadId);
945
945
  // Preserve the invoking chat as provenance/fallback for this run.
946
- // An explicitly configured results destination continues to win.
946
+ // The routine's results destination continues to win.
947
947
  if (request)
948
948
  run.sourceThreadId = request.threadId;
949
949
  if (request)
@@ -1297,7 +1297,7 @@ export class RoutineManager {
1297
1297
  ? run.groupId
1298
1298
  ? this.options.createGoalTask?.(run.groupId, title) ?? null
1299
1299
  : null
1300
- : this.options.createTask(run.botId, title, run.triggerSource === "webhook", run.routineId);
1300
+ : this.options.createTask(run.botId, title, run.triggerSource === "webhook", run);
1301
1301
  if (!task) {
1302
1302
  this.failRun(run, run.target === "room-goal"
1303
1303
  ? "Could not create a room task for this goal"
@@ -1546,7 +1546,7 @@ export class RoutineManager {
1546
1546
  if (value === null) {
1547
1547
  const destination = this.options.resolveResultsThread?.(routine, true);
1548
1548
  if (!destination)
1549
- throw new Error("Could not create a results thread for this routine");
1549
+ throw new Error("Could not find a results thread for this routine");
1550
1550
  routine.resultsThreadId = destination;
1551
1551
  return () => this.options.discardResultsThread?.(routine.botId, destination);
1552
1552
  }
@@ -7,9 +7,7 @@
7
7
  // shell command over computer_exec or a status read does not count,
8
8
  // however the driver happens to spell the tool's name;
9
9
  // 2. is the end frame different from the one the transcript already
10
- // shows — a boxAgent turn starts as screen work by definition, so this
11
- // is what keeps its shell-only replies from re-picturing the same idle
12
- // desktop.
10
+ // shows — this keeps a reply from re-picturing the same idle desktop.
13
11
  import { createHash } from "node:crypto";
14
12
  // Tool-name classification lives in shared/tool-surface.ts so the renderer
15
13
  // reads a tool the same way the poller does.
@@ -29,7 +29,7 @@ import { createHash, randomUUID } from "node:crypto";
29
29
  import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
30
30
  import { dirname, join, relative, resolve } from "node:path";
31
31
  import { z } from "zod";
32
- import { writeFileAtomic } from "./atomic.js";
32
+ import { writeFileAtomic, writeFileAtomicIfChanged } from "./atomic.js";
33
33
  import { DATA_DIR } from "./config.js";
34
34
  import { redactSecretsInText } from "./redact.js";
35
35
  import { LEARN_SOURCE_PREFIX } from "./skill-learn.js";
@@ -191,9 +191,10 @@ function readManagedLinks(botId) {
191
191
  return [];
192
192
  }
193
193
  }
194
+ // Rebuilt on every turn and almost always the same list.
194
195
  function writeManagedLinks(botId, names) {
195
196
  mkdirSync(skillStateDir(botId), { recursive: true, mode: 0o700 });
196
- writeFileAtomic(managedLinksPath(botId), `${JSON.stringify([...new Set(names)].sort(), null, 2)}\n`, { mode: 0o600 });
197
+ writeFileAtomicIfChanged(managedLinksPath(botId), `${JSON.stringify([...new Set(names)].sort(), null, 2)}\n`, { mode: 0o600 });
197
198
  }
198
199
  function manifestFromFile(path) {
199
200
  try {