openmausbot 0.1.93 → 0.1.95

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 (192) hide show
  1. package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-BwWpc4RK.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
  3. package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
  7. package/dist/assets/channel-DS2NbT5x.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
  34. package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
  35. package/dist/assets/index-NF7XCHAy.css +1 -0
  36. package/dist/assets/index-X_5ydXIE.js +610 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
  41. package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
  42. package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +3 -1
  62. package/dist-server/companion/src/routes.js +60 -0
  63. package/dist-server/container-mcp.js +94 -77
  64. package/dist-server/drivers/agents-proxy.js +96 -39
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +3 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
  68. package/dist-server/index.js +18242 -13645
  69. package/dist-server/local-computer.js +3 -1
  70. package/dist-server/mcp-gate.js +147 -19
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +9618 -10058
  73. package/dist-server/pair-cli.js +9618 -10058
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +3 -1
  76. package/dist-server/server/account-signin.js +0 -2
  77. package/dist-server/server/activity.js +201 -0
  78. package/dist-server/server/auto-approve.js +39 -11
  79. package/dist-server/server/automatic-recovery.js +1 -1
  80. package/dist-server/server/boat-create-idempotency.js +0 -7
  81. package/dist-server/server/boat-delete-journal.js +0 -4
  82. package/dist-server/server/bot-package.js +1 -19
  83. package/dist-server/server/browser-live.js +14 -5
  84. package/dist-server/server/browser-runtime.js +7 -2
  85. package/dist-server/server/channel-queue.js +4 -1
  86. package/dist-server/server/chief-of-staff.js +2 -3
  87. package/dist-server/server/cli.js +32 -12
  88. package/dist-server/server/cloud-backend.js +0 -4
  89. package/dist-server/server/cloud-computer-tools.js +110 -0
  90. package/dist-server/server/cloud-home.js +0 -1
  91. package/dist-server/server/cloud-lending.js +0 -3
  92. package/dist-server/server/cloud-move-http.js +9 -3
  93. package/dist-server/server/cloud-move.js +18 -0
  94. package/dist-server/server/composio.js +31 -2
  95. package/dist-server/server/computer-control.js +0 -15
  96. package/dist-server/server/computer-observation.js +0 -74
  97. package/dist-server/server/computer-wait.js +33 -13
  98. package/dist-server/server/config.js +73 -11
  99. package/dist-server/server/container-computer.js +7 -19
  100. package/dist-server/server/contracts.js +9 -0
  101. package/dist-server/server/control-client.js +4 -10
  102. package/dist-server/server/decision-log.js +9 -6
  103. package/dist-server/server/delegations.js +47 -22
  104. package/dist-server/server/drivers/acp/core.js +64 -13
  105. package/dist-server/server/drivers/acp/grok.js +208 -3
  106. package/dist-server/server/drivers/agents-call.js +70 -29
  107. package/dist-server/server/drivers/agents-catalog.js +34 -12
  108. package/dist-server/server/drivers/agents-proxy.js +8 -30
  109. package/dist-server/server/drivers/antigravity.js +1 -4
  110. package/dist-server/server/drivers/boatagent.js +25 -4
  111. package/dist-server/server/drivers/cerebras.js +3 -3
  112. package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
  113. package/dist-server/server/drivers/claude.js +33 -19
  114. package/dist-server/server/drivers/codex.js +84 -10
  115. package/dist-server/server/drivers/openai-chat.js +14 -9
  116. package/dist-server/server/drivers/pi.js +48 -4
  117. package/dist-server/server/drivers/retry.js +8 -4
  118. package/dist-server/server/engine-install.js +1 -5
  119. package/dist-server/server/environment.js +3 -0
  120. package/dist-server/server/guarded-requests.js +2 -1
  121. package/dist-server/server/harness/registry.js +0 -1
  122. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  123. package/dist-server/server/index.js +1929 -507
  124. package/dist-server/server/lending-memory.js +12 -8
  125. package/dist-server/server/live-call-controller.js +868 -0
  126. package/dist-server/server/live-call.js +216 -0
  127. package/dist-server/server/mcp-gate-config.js +43 -12
  128. package/dist-server/server/mcp-gate.js +127 -25
  129. package/dist-server/server/mcp-http.js +46 -8
  130. package/dist-server/server/mcp-remote-proxy.js +108 -0
  131. package/dist-server/server/memory-entries.js +38 -8
  132. package/dist-server/server/memory-organize.js +1 -8
  133. package/dist-server/server/memory-topics.js +4 -4
  134. package/dist-server/server/memory-upkeep.js +13 -15
  135. package/dist-server/server/message-file.js +5 -9
  136. package/dist-server/server/new-bot-defaults.js +3 -0
  137. package/dist-server/server/outbound-counts.js +79 -0
  138. package/dist-server/server/outbound-requests.js +60 -0
  139. package/dist-server/server/package-import.js +1 -1
  140. package/dist-server/server/peer-delivery.js +1 -0
  141. package/dist-server/server/peer-provenance.js +4 -2
  142. package/dist-server/server/permission-proxy.js +15 -1
  143. package/dist-server/server/plan-usage.js +797 -0
  144. package/dist-server/server/proxy-paths.js +3 -1
  145. package/dist-server/server/redact.js +2 -0
  146. package/dist-server/server/request-auth.js +3 -2
  147. package/dist-server/server/room-handoffs.js +5 -13
  148. package/dist-server/server/routes/live.js +98 -0
  149. package/dist-server/server/routine-requests.js +1 -1
  150. package/dist-server/server/routines.js +5 -1
  151. package/dist-server/server/setup-mode.js +0 -2
  152. package/dist-server/server/skill-learn.js +0 -1
  153. package/dist-server/server/skill-library.js +195 -3
  154. package/dist-server/server/skills-library-migration.js +235 -0
  155. package/dist-server/server/skills.js +66 -3
  156. package/dist-server/server/steer-queue.js +13 -1
  157. package/dist-server/server/store.js +41 -5
  158. package/dist-server/server/surface.js +74 -23
  159. package/dist-server/server/system-prompt.js +4 -4
  160. package/dist-server/server/team-backup.js +2 -1
  161. package/dist-server/server/team-memory.js +209 -0
  162. package/dist-server/server/thread-events.js +3 -3
  163. package/dist-server/server/tunnel.js +20 -1
  164. package/dist-server/server/turn-log.js +10 -0
  165. package/dist-server/server/turn-resources.js +124 -0
  166. package/dist-server/server/vps-computer.js +4 -14
  167. package/dist-server/server/workspace.js +85 -47
  168. package/dist-server/shared/activity.js +3 -0
  169. package/dist-server/shared/ask-question.js +13 -0
  170. package/dist-server/shared/bot-avatar.js +8 -0
  171. package/dist-server/shared/call-consent.js +28 -0
  172. package/dist-server/shared/connector-scopes.js +78 -0
  173. package/dist-server/shared/live-approval.js +124 -0
  174. package/dist-server/shared/live-call.js +176 -0
  175. package/dist-server/shared/outbound.js +128 -0
  176. package/dist-server/shared/skill-md.js +10 -0
  177. package/dist-server/shared/team-backup.js +3 -0
  178. package/dist-server/shared/tool-scope-support.js +25 -0
  179. package/dist-server/shared/tool-scope.js +136 -0
  180. package/dist-server/shared/tool-surface.js +3 -2
  181. package/dist-server/src/lib/thread-refs.js +406 -0
  182. package/dist-server/vps-container-mcp.js +94 -77
  183. package/dist-server/workspace-backup.worker.js +89 -100
  184. package/enterprise/server/index.js +3 -1
  185. package/package.json +1 -1
  186. package/dist/assets/channel-BU94VpxE.js +0 -1
  187. package/dist/assets/index-Biji-bfL.css +0 -1
  188. package/dist/assets/index-DZlhoES0.js +0 -597
  189. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
  190. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  191. package/dist-server/server/drivers/grokagent.js +0 -4
  192. package/dist-server/server/openmaus-status-capsule.js +0 -329
@@ -31,7 +31,8 @@ export function resolveProxy(relative) {
31
31
  * the smoke test can assert each one actually exists in a packaged layout —
32
32
  * the check that would have caught the 0.1.24 breakage. */
33
33
  export const SPAWNED_PROXIES = {
34
- browser: resolveProxy("browser-proxy"),
34
+ // The built-in browser and the cloud computer: argv[2] picks which.
35
+ harnessMcp: resolveProxy("harness-mcp-proxy"),
35
36
  localComputer: resolveProxy("local-computer-proxy"),
36
37
  permission: resolveProxy("permission-proxy"),
37
38
  containerMcp: resolveProxy("container-mcp"),
@@ -40,6 +41,7 @@ export const SPAWNED_PROXIES = {
40
41
  dweb: resolveProxy("drivers/dweb-proxy"),
41
42
  connectors: resolveProxy("connector-proxy"),
42
43
  mcpGate: resolveProxy("mcp-gate"),
44
+ mcpRemote: resolveProxy("mcp-remote-proxy"),
43
45
  phone: resolveProxy("drivers/phone-proxy"),
44
46
  hook: resolveProxy("hooks/omb-hook"),
45
47
  // Loaded by the external `pi` process via `-e`, not by this server — but
@@ -18,6 +18,8 @@ const SECRET_KEY_PARTS = ["token", "secret", "password", "passwd", "apikey", "ap
18
18
  * every real one is spelled (API_KEY, consumer-key, xai_key). */
19
19
  function isSecretName(name) {
20
20
  const lower = name.toLowerCase();
21
+ if (lower === "omb_gate_upstream" || lower === "omb_remote_mcp_server")
22
+ return true;
21
23
  if (SECRET_KEY_PARTS.some((part) => lower.includes(part)))
22
24
  return true;
23
25
  return /(^|[_.-])keys?$/.test(lower);
@@ -1,6 +1,6 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { isIP } from "node:net";
3
- import { denyReason as companionDenial } from "../companion/src/routes.js";
3
+ import { denyReason as companionDenial, isCompanionNotice } from "../companion/src/routes.js";
4
4
  const LOOPBACK_SCOPES = ["admin", "client"];
5
5
  const SERVICE_SCOPES = ["client"];
6
6
  /** What a `service`-trust loopback caller may reach, and nothing else:
@@ -442,7 +442,8 @@ export function resolveRequestAuth(req, options) {
442
442
  if (!secureTokenMatch(companionToken, options.companionMutationToken ?? "") ||
443
443
  req.headers["x-openmausbot-companion"] !== "1" ||
444
444
  !/^[\w-]{1,128}$/.test(headerValue(req.headers["x-openmausbot-companion-device"]) ?? "") ||
445
- companionDenial({ path, method, authenticated: true }))
445
+ // a phone's allowlisted route, or the companion's own notice (an unpaired phone)
446
+ (companionDenial({ path, method, authenticated: true }) && !isCompanionNotice(method, path)))
446
447
  return deny(403, "forbidden: invalid companion request");
447
448
  return { auth: { kind: "loopback", scopes: LOOPBACK_SCOPES }, status: 401, error: "" };
448
449
  }
@@ -225,20 +225,12 @@ export class RoomHandoffs {
225
225
  if (path.some(n => n.botId === target.botId && (!n.groupId || !target.groupId || n.groupId === target.groupId))) {
226
226
  throw new Error("Cannot assign work back to an ancestor; results return automatically");
227
227
  }
228
- const existing = this.children(parent.id).find(n => n.key === key);
229
- if (existing) {
230
- if (existing.groupId !== target.groupId || existing.botId !== target.botId || existing.text !== text ||
231
- existing.kind !== kind || existing.requestBatchKey !== requestBatchKey)
232
- throw new Error("request_key was already used for different work");
228
+ // The same text to the same teammate and place under one parent is one
229
+ // request, so a repeat lands on it. Only rework=true runs a finished or
230
+ // failed one again.
231
+ const existing = this.children(parent.id).findLast(n => n.botId === target.botId && n.groupId === target.groupId && n.text === text);
232
+ if (existing && !(rework && terminal(existing)))
233
233
  return { node: existing, duplicate: true };
234
- }
235
- if (requestBatchKey && target.groupId) {
236
- const batch = this.children(parent.id).filter(n => n.requestBatchKey === requestBatchKey && n.groupId === target.groupId);
237
- if (batch.some(n => n.text !== text || n.threadId !== target.threadId))
238
- throw new Error("request_key was already used for different room work");
239
- if (batch.some(n => n.startedAt !== undefined))
240
- throw new Error("This shared room request has already started; use a new request_key for additional recipients");
241
- }
242
234
  if (!rework && this.children(parent.id).some(n => n.kind === kind &&
243
235
  n.groupId === target.groupId && n.botId === target.botId && n.status === "completed")) {
244
236
  throw new Error("This agent already completed your assignment. Do not send acknowledgements or approvals as new work. Finish with your decision; results return automatically. Only use rework=true for concrete additional work.");
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { LiveSessionError, MAX_SDP_BYTES } from "../live-call.js";
3
+ import { LiveCallBusyError } from "../live-call-controller.js";
4
+ import { PASS } from "./table.js";
5
+ const id = z.string().regex(/^[\w-]{1,120}$/);
6
+ const sessionBody = z.object({
7
+ botId: id,
8
+ threadId: id.optional(),
9
+ // Not trimmed: the SDP offer goes to OpenAI byte for byte.
10
+ sdp: z.string().min(1).refine((sdp) => Buffer.byteLength(sdp) <= MAX_SDP_BYTES),
11
+ client: z.enum(["desktop", "ios", "android"]),
12
+ }).strict();
13
+ const endBody = z.object({ callId: z.string().min(1).max(120) }).strict();
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.
16
+ const settingsBody = z.object({
17
+ voice: z.string().trim().max(40).regex(/^[a-z]*$/).optional(),
18
+ readTypedReplies: z.boolean().optional(),
19
+ idleMinutes: z.number().int().min(1).max(60).optional(),
20
+ }).strict();
21
+ const DEVICE_ID = /^[\w-]{1,128}$/;
22
+ /** The paired phone the companion vouched for, or undefined. The companion
23
+ * sends its own id for the phone that authenticated (never one the phone
24
+ * chose); in the desktop app the harness has already checked the companion's
25
+ * private token before this runs (server/request-auth.ts). */
26
+ function companionDevice(req) {
27
+ if (req.headers["x-openmausbot-companion"] !== "1")
28
+ return undefined;
29
+ const device = req.headers["x-openmausbot-companion-device"];
30
+ return typeof device === "string" && DEVICE_ID.test(device) ? device : undefined;
31
+ }
32
+ /** The parsed JSON body, or undefined when it is not JSON (the schema then refuses it). */
33
+ async function bodyOf(req, readBody) {
34
+ try {
35
+ return await readBody(req);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ export function createLiveRoutes(deps) {
42
+ return async ({ req, res, path, method, auth, json, readBody }) => {
43
+ if (!path.startsWith("/api/live/"))
44
+ return PASS;
45
+ if (method === "POST" && path === "/api/live/session") {
46
+ const parsed = sessionBody.safeParse(await bodyOf(req, readBody));
47
+ if (!parsed.success)
48
+ return json(res, 400, { error: "The call request was not valid." });
49
+ const target = deps.resolveTarget(parsed.data.botId, parsed.data.threadId);
50
+ if (!target)
51
+ return json(res, 404, { error: "That bot or chat does not exist." });
52
+ try {
53
+ // A phone's call is bound to the phone, so unpairing it ends the call.
54
+ const device = companionDevice(req);
55
+ const { call, sdp } = await deps.calls.start({ auth, ...(device ? { device } : {}), ...target, client: parsed.data.client, sdp: parsed.data.sdp });
56
+ return json(res, 201, { call, transport: { type: "webrtc", sdp } });
57
+ }
58
+ catch (error) {
59
+ if (error instanceof LiveCallBusyError)
60
+ return json(res, 409, { error: error.message, activeCall: error.call });
61
+ if (error instanceof LiveSessionError) {
62
+ // The one 409 a session refuses with is a missing key.
63
+ return json(res, error.status, error.status === 409 ? { error: error.message, needsKey: true } : { error: error.message });
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+ if (method === "POST" && path === "/api/live/call/end") {
69
+ const parsed = endBody.safeParse(await bodyOf(req, readBody));
70
+ if (!parsed.success)
71
+ return json(res, 400, { error: "The request was not valid." });
72
+ const call = await deps.calls.end(parsed.data.callId);
73
+ return call ? json(res, 200, { call }) : json(res, 404, { error: "That call is not running." });
74
+ }
75
+ if (method === "GET" && path === "/api/live/call")
76
+ return json(res, 200, { call: deps.calls.current() });
77
+ // The companion unpaired a phone: end the call that phone holds, if any.
78
+ if (method === "POST" && path === "/api/live/device-revoked") {
79
+ if (req.headers["x-openmausbot-companion"] !== "1")
80
+ return json(res, 403, { error: "Only the phone companion can report an unpaired phone." });
81
+ const device = companionDevice(req);
82
+ if (!device)
83
+ return json(res, 400, { error: "The unpaired phone was not named." });
84
+ return json(res, 200, { call: deps.calls.deviceRevoked(device) });
85
+ }
86
+ if (method === "PATCH" && path === "/api/live/settings") {
87
+ const parsed = settingsBody.safeParse(await bodyOf(req, readBody));
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." });
90
+ }
91
+ // as PUT /api/config: an empty patch is not saved (or broadcast)
92
+ if (!Object.keys(parsed.data).length)
93
+ return json(res, 400, { error: "nothing to save" });
94
+ return json(res, 200, { live: await deps.saveSettings(parsed.data) });
95
+ }
96
+ return PASS;
97
+ };
98
+ }
@@ -677,7 +677,7 @@ function cardCopy(operation, manager, timeZone, now) {
677
677
  const nextRunAt = nextForOperation(operation, manager, now);
678
678
  const scheduleTimeZone = definition.schedule.type === "cron" ? definition.schedule.timeZone : timeZone;
679
679
  const when = operation.action === "run_now" ? "Now" : scheduleText(definition.schedule, timeZone);
680
- const destination = definition.runOn === "cloud" ? "Boat-hosted agent" : "Bot’s current model and configured computer";
680
+ const destination = definition.runOn === "cloud" ? "Bot’s current model on its Boat cloud computer" : "Bot’s current model and configured computer";
681
681
  const current = operation.action === "create"
682
682
  ? null
683
683
  : manager.listRoutines().find((routine) => routine.id === operation.routineId) ?? null;
@@ -1381,7 +1381,11 @@ export class RoutineManager {
1381
1381
  if (event.denials?.length)
1382
1382
  run.denials = [...new Set([...(run.denials ?? []), ...event.denials])];
1383
1383
  if (!event.ok) {
1384
- this.failRun(run, event.stopReason ?? run.error ?? "The bot did not complete this run");
1384
+ const genericStopReason = event.stopReason === "error" || event.stopReason === "tool_error";
1385
+ this.failRun(run, (genericStopReason ? run.error : undefined) ??
1386
+ event.stopReason ??
1387
+ run.error ??
1388
+ "The bot did not complete this run");
1385
1389
  queueMicrotask(() => void this.tick());
1386
1390
  return cloneRun(run);
1387
1391
  }
@@ -51,8 +51,6 @@ function buildSetupPrompt(profileAside, cwd) {
51
51
  " Full Access does not supply answers, credentials, or broader permissions for another bot. A credential request still needs the user's secure entry. Report failed or cancelled changes honestly." +
52
52
  " Finish by saying exactly what remains for the user to do by hand — authorizing an app or account (OAuth), creating a third-party application or bot token, or deciding a pending review — and point them to the Access section of the bot's settings for the app connections.");
53
53
  }
54
- /** The setup block naming skill_manage, for a turn with skill authoring on. */
55
- export const SETUP_PROMPT = buildSetupPrompt(SKILL_MANAGE_ASIDE);
56
54
  export function setupSystemPrompt(active, options) {
57
55
  if (!active)
58
56
  return "";
@@ -6,7 +6,6 @@
6
6
  // prompt and recognises the slash command, so it works on every engine
7
7
  // that mounts the agents tools.
8
8
  import { SAVE_RUN_AS_SKILL_LINE } from "../shared/learn-request.js";
9
- export const LEARN_COMMAND = "/learn";
10
9
  export const LEARN_SOURCE_PREFIX = "learn:";
11
10
  export const LEARN_PROMPT_MARKER = "[/learn]";
12
11
  export const LEARN_DESCRIPTION_SOFT_MAX = 60;
@@ -4,6 +4,13 @@
4
4
  // declared trigger terms and mounted capabilities.
5
5
  import { existsSync, readFileSync, readdirSync } from "node:fs";
6
6
  import { basename, join } from "node:path";
7
+ import { createHash } from "node:crypto";
8
+ import { EventEmitter } from "node:events";
9
+ import { mkdirSync } from "node:fs";
10
+ import { z } from "zod";
11
+ import { writeFileAtomic } from "./atomic.js";
12
+ import { DATA_DIR } from "./config.js";
13
+ import { isSkillName, parseSkillMd, SKILL_FILE_MAX_BYTES } from "../shared/skill-md.js";
7
14
  const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
15
  function strings(value) {
9
16
  return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim())
@@ -99,9 +106,6 @@ export function mergeSkills(bundled, user) {
99
106
  }
100
107
  return [...byId.values()];
101
108
  }
102
- export function skillInstructionsFor(text, capabilities, skills, options) {
103
- return renderSkillInstructions(selectBundledSkills(text, capabilities, skills), options);
104
- }
105
109
  export function selectBundledSkills(text, capabilities, skills) {
106
110
  const haystack = text.toLowerCase();
107
111
  const available = new Set(capabilities);
@@ -114,3 +118,191 @@ export function renderSkillInstructions(selected, { includeRoot = false } = {})
114
118
  return "";
115
119
  return selected.map(({ manifest, instructions, directory }) => `\n\n<openmaus-skill id=${JSON.stringify(manifest.id)} version=${JSON.stringify(manifest.version)}${includeRoot ? ` root=${JSON.stringify(directory)}` : ""}>\n${instructions}\n</openmaus-skill>`).join("");
116
120
  }
121
+ const skillLibraryEntrySchema = z.object({
122
+ name: z.string(),
123
+ description: z.string(),
124
+ source: z.string(),
125
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
126
+ importedAt: z.string(),
127
+ reviewState: z.enum(["approved", "disabled"]),
128
+ license: z.string().optional(),
129
+ compatibility: z.string().optional(),
130
+ tags: z.array(z.string().regex(/^[a-z0-9][a-z0-9-]{0,23}$/)).max(8).optional(),
131
+ warnings: z.array(z.string()),
132
+ skippedFiles: z.array(z.string()),
133
+ package: z.object({
134
+ installId: z.string().regex(/^[a-f0-9]{32}$/),
135
+ key: z.string().min(1).max(64),
136
+ release: z.string().min(1).max(40),
137
+ r: z.string().regex(/^[a-f0-9]{64}$/),
138
+ w: z.string().regex(/^[a-f0-9]{64}$/),
139
+ via: z.literal("preset").optional(),
140
+ }).optional(),
141
+ });
142
+ const skillLibraryIndexSchema = z.record(z.string(), skillLibraryEntrySchema);
143
+ /** Every library write emits here so later skills-lane consumers can drop
144
+ * caches without polling. The payload names what changed. */
145
+ export const skillLibraryEvents = new EventEmitter();
146
+ export function skillsLibraryRoot(dataDir = DATA_DIR) {
147
+ return join(dataDir, "skills-library");
148
+ }
149
+ function libraryIndexPath(root) {
150
+ return join(root, "index.json");
151
+ }
152
+ export function librarySkillDirectory(root, name) {
153
+ return join(root, "skills", name);
154
+ }
155
+ /** The path agents are told to read for an assigned library skill. */
156
+ export function librarySkillFilePath(name, root = skillsLibraryRoot()) {
157
+ return join(root, "skills", name, "SKILL.md");
158
+ }
159
+ /** Where a migrated per-bot copy is preserved. Archives are write-once:
160
+ * migration never deletes the original bytes, it moves them here. */
161
+ export function libraryArchiveDirectory(root, botId, name) {
162
+ return join(root, "archive", botId, name);
163
+ }
164
+ /** A missing index is a legitimate empty library; an index that exists but
165
+ * cannot be read or validated is not, and writers must refuse rather than
166
+ * replace it and strand every stored skill. */
167
+ function readSkillLibraryIndexState(root) {
168
+ try {
169
+ const parsed = JSON.parse(readFileSync(libraryIndexPath(root), "utf8"));
170
+ const result = skillLibraryIndexSchema.safeParse(parsed);
171
+ return result.success
172
+ ? { index: result.data }
173
+ : { error: "the skills library index is invalid" };
174
+ }
175
+ catch (error) {
176
+ if (error.code === "ENOENT")
177
+ return { index: {} };
178
+ return { error: "the skills library index could not be read" };
179
+ }
180
+ }
181
+ export function readSkillLibraryIndex(root = skillsLibraryRoot()) {
182
+ const result = readSkillLibraryIndexState(root);
183
+ return "error" in result ? {} : result.index;
184
+ }
185
+ function writeSkillLibraryIndex(root, index) {
186
+ mkdirSync(root, { recursive: true, mode: 0o700 });
187
+ writeFileAtomic(libraryIndexPath(root), `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
188
+ }
189
+ function sha256Hex(text) {
190
+ return createHash("sha256").update(text).digest("hex");
191
+ }
192
+ function libraryContentMatches(root, entry) {
193
+ try {
194
+ const file = join(librarySkillDirectory(root, entry.name), "SKILL.md");
195
+ if (!existsSync(file))
196
+ return false;
197
+ return sha256Hex(readFileSync(file, "utf8")) === entry.sha256;
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ function librarySkillListing(root, entry) {
204
+ const intact = libraryContentMatches(root, entry);
205
+ return {
206
+ name: entry.name,
207
+ description: entry.description,
208
+ enabled: entry.reviewState === "approved" && intact,
209
+ editable: false,
210
+ source: entry.source,
211
+ sha256: entry.sha256,
212
+ importedAt: entry.importedAt,
213
+ ...(entry.license ? { license: entry.license } : {}),
214
+ ...(entry.compatibility ? { compatibility: entry.compatibility } : {}),
215
+ tags: entry.tags ?? [],
216
+ warnings: intact
217
+ ? entry.warnings
218
+ : [...entry.warnings, "stored SKILL.md changed after review — enablement is blocked"],
219
+ skippedFiles: entry.skippedFiles,
220
+ };
221
+ }
222
+ export function listLibrarySkills(root = skillsLibraryRoot()) {
223
+ return Object.values(readSkillLibraryIndex(root))
224
+ .map((entry) => librarySkillListing(root, entry))
225
+ .sort((a, b) => a.name.localeCompare(b.name));
226
+ }
227
+ /** Put a skill in the library. Installing the same name with the same
228
+ * SKILL.md bytes is idempotent (the existing entry wins); the same name
229
+ * with different bytes is refused, mirroring the per-bot import rule.
230
+ * Every failure returns `{ error }` — callers fall back. */
231
+ export function installLibrarySkill(input) {
232
+ const root = input.root ?? skillsLibraryRoot();
233
+ if (!isSkillName(input.name))
234
+ return { error: "invalid skill name" };
235
+ if (Buffer.byteLength(input.instructions, "utf8") > SKILL_FILE_MAX_BYTES) {
236
+ return { error: `SKILL.md is larger than ${SKILL_FILE_MAX_BYTES / 1024}KB` };
237
+ }
238
+ const parsed = parseSkillMd(input.instructions);
239
+ if ("error" in parsed)
240
+ return parsed;
241
+ if (parsed.name !== input.name)
242
+ return { error: `the skill is named "${parsed.name}", not "${input.name}"` };
243
+ const sha256 = sha256Hex(input.instructions);
244
+ const indexRead = readSkillLibraryIndexState(root);
245
+ if ("error" in indexRead)
246
+ return indexRead;
247
+ const index = indexRead.index;
248
+ const existing = index[input.name];
249
+ if (existing) {
250
+ if (existing.sha256 === sha256)
251
+ return librarySkillListing(root, existing);
252
+ return { error: `a skill named "${input.name}" is already in the library — choose a different name` };
253
+ }
254
+ // Callers such as the library migration do not pass tags; frontmatter
255
+ // tags parsed from the instructions are the source of record then.
256
+ const tags = input.tags?.length ? input.tags : parsed.tags;
257
+ const entry = {
258
+ name: input.name,
259
+ description: input.description?.trim() || parsed.description,
260
+ source: input.source,
261
+ sha256,
262
+ importedAt: new Date().toISOString(),
263
+ reviewState: input.reviewState ?? "disabled",
264
+ ...(input.license ? { license: input.license } : {}),
265
+ ...(input.compatibility ? { compatibility: input.compatibility } : {}),
266
+ ...(tags?.length ? { tags } : {}),
267
+ warnings: input.warnings ?? [],
268
+ skippedFiles: [],
269
+ ...(input.package ? { package: { ...input.package } } : {}),
270
+ };
271
+ // Bytes first, index second: the index is the commit point, so a crash
272
+ // between the two leaves unreferenced bytes, never a dangling entry.
273
+ mkdirSync(librarySkillDirectory(root, input.name), { recursive: true, mode: 0o700 });
274
+ writeFileAtomic(join(librarySkillDirectory(root, input.name), "SKILL.md"), input.instructions, { mode: 0o600 });
275
+ index[input.name] = entry;
276
+ writeSkillLibraryIndex(root, index);
277
+ skillLibraryEvents.emit("invalidate", { kind: "install", name: input.name });
278
+ return librarySkillListing(root, entry);
279
+ }
280
+ export function readLibrarySkillFile(name, root = skillsLibraryRoot()) {
281
+ if (!isSkillName(name))
282
+ return null;
283
+ const entry = readSkillLibraryIndex(root)[name];
284
+ if (!entry)
285
+ return null;
286
+ try {
287
+ const text = readFileSync(join(librarySkillDirectory(root, name), "SKILL.md"), "utf8");
288
+ return sha256Hex(text) === entry.sha256 ? text : null;
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ }
294
+ export function setLibrarySkillReviewState(name, reviewState, root = skillsLibraryRoot()) {
295
+ if (!isSkillName(name))
296
+ return { error: "invalid skill name" };
297
+ const index = readSkillLibraryIndex(root);
298
+ const entry = index[name];
299
+ if (!entry)
300
+ return { error: `no library skill named "${name}"` };
301
+ if (reviewState === "approved" && !libraryContentMatches(root, entry)) {
302
+ return { error: "stored SKILL.md changed after review — the library entry cannot be approved" };
303
+ }
304
+ entry.reviewState = reviewState;
305
+ writeSkillLibraryIndex(root, index);
306
+ skillLibraryEvents.emit("invalidate", { kind: "review-state", name });
307
+ return librarySkillListing(root, entry);
308
+ }