openmausbot 0.1.85 → 0.1.86

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 (193) hide show
  1. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  2. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  4. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  10. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  16. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  19. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  28. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  31. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  34. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  43. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  46. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  49. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  52. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  54. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  57. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  60. package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
  61. package/dist/assets/arc-DW7mrI4p.js +1 -0
  62. package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
  63. package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
  64. package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
  65. package/dist/assets/channel-BXFkgus9.js +1 -0
  66. package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
  67. package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
  68. package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
  69. package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
  70. package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
  71. package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
  72. package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
  73. package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
  74. package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
  75. package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
  76. package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
  77. package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
  78. package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
  79. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  80. package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
  81. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  82. package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
  83. package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
  84. package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
  85. package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
  86. package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
  87. package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
  88. package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
  89. package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
  90. package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
  91. package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
  92. package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
  93. package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
  94. package/dist/assets/graph-D8UmR1vK.js +1 -0
  95. package/dist/assets/index-BGgqlWPJ.css +1 -0
  96. package/dist/assets/index-BtHo8ZKl.js +577 -0
  97. package/dist/assets/{index-CTNNoqSJ.js → index-h9FFNwTu.js} +1 -1
  98. package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
  99. package/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
  101. package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
  102. package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
  103. package/dist/assets/linear-3su2TG93.js +1 -0
  104. package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
  105. package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
  106. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  107. package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
  108. package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
  109. package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
  110. package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
  111. package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
  112. package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
  113. package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
  114. package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
  115. package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
  116. package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
  117. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
  118. package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
  119. package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
  120. package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
  121. package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
  122. package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
  123. package/dist/index.html +2 -2
  124. package/dist-server/container-mcp.js +19 -10
  125. package/dist-server/drivers/agents-proxy.js +16159 -1596
  126. package/dist-server/enterprise/LICENSE +32 -0
  127. package/dist-server/enterprise/server/index.js +5 -5
  128. package/dist-server/index.js +4779 -1667
  129. package/dist-server/mcp-server.js +3 -1
  130. package/dist-server/openmausbot.js +4679 -4266
  131. package/dist-server/pair-cli.js +4679 -4266
  132. package/dist-server/server/admin-activity.js +410 -0
  133. package/dist-server/server/bot-visibility.js +462 -0
  134. package/dist-server/server/box.js +2 -1
  135. package/dist-server/server/browser-runtime.js +1 -1
  136. package/dist-server/server/channel-queue.js +2 -0
  137. package/dist-server/server/cli-prompts.js +24 -2
  138. package/dist-server/server/cli.js +79 -10
  139. package/dist-server/server/composio.js +49 -11
  140. package/dist-server/server/config.js +79 -13
  141. package/dist-server/server/decision-log.js +209 -48
  142. package/dist-server/server/default-model-selection.js +20 -3
  143. package/dist-server/server/delegations.js +11 -9
  144. package/dist-server/server/drivers/acp/core.js +110 -22
  145. package/dist-server/server/drivers/acp/grok.js +4 -3
  146. package/dist-server/server/drivers/agents-call.js +1123 -0
  147. package/dist-server/server/drivers/agents-catalog.js +659 -0
  148. package/dist-server/server/drivers/agents-client.js +28 -0
  149. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  150. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  151. package/dist-server/server/drivers/claude.js +21 -6
  152. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  153. package/dist-server/server/drivers/codex.js +26 -4
  154. package/dist-server/server/drivers/grok.js +2 -1
  155. package/dist-server/server/drivers/openai-compat.js +15 -8
  156. package/dist-server/server/enterprise.js +97 -16
  157. package/dist-server/server/external-runtime.js +83 -0
  158. package/dist-server/server/guarded-requests.js +60 -0
  159. package/dist-server/server/harness/http.js +61 -0
  160. package/dist-server/server/hosted-models.js +151 -0
  161. package/dist-server/server/hosted-slack.js +11 -0
  162. package/dist-server/server/index.js +1718 -348
  163. package/dist-server/server/managed-desktop.js +79 -14
  164. package/dist-server/server/managed-policy.js +153 -0
  165. package/dist-server/server/message-db.js +28 -0
  166. package/dist-server/server/model-context-window.js +7 -0
  167. package/dist-server/server/model-prices.js +121 -0
  168. package/dist-server/server/notify.js +10 -1
  169. package/dist-server/server/peer-roster.js +13 -2
  170. package/dist-server/server/recent-work.js +15 -0
  171. package/dist-server/server/request-auth.js +96 -0
  172. package/dist-server/server/room-handoffs.js +25 -11
  173. package/dist-server/server/routes/hosted-slack.js +20 -0
  174. package/dist-server/server/routes/table.js +14 -0
  175. package/dist-server/server/routines.js +22 -0
  176. package/dist-server/server/spend.js +111 -16
  177. package/dist-server/server/steer-queue.js +6 -1
  178. package/dist-server/server/store.js +292 -21
  179. package/dist-server/server/team-backup.js +10 -1
  180. package/dist-server/server/thread-starters.js +53 -0
  181. package/dist-server/server/usage-ledger.js +31 -8
  182. package/dist-server/server/workspace-backup-policy.js +9 -2
  183. package/dist-server/server/workspace-backup.js +7 -4
  184. package/dist-server/shared/mention-boundary.js +15 -0
  185. package/dist-server/shared/routines.js +7 -1
  186. package/dist-server/shared/team-backup.js +2 -0
  187. package/dist-server/src/lib/composer-attachments.js +16 -0
  188. package/dist-server/vps-container-mcp.js +19 -10
  189. package/enterprise/README.md +33 -12
  190. package/enterprise/server/index.js +5 -5
  191. package/package.json +1 -1
  192. package/dist/assets/index-Ba9G44HI.js +0 -310
  193. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -23,6 +23,7 @@
23
23
  // This module only exports; openmausbot.ts is the entry that runs main(), so
24
24
  // bundling this file into other entries (pair-cli.ts) never runs it twice.
25
25
  import { spawn } from "node:child_process";
26
+ import { randomBytes } from "node:crypto";
26
27
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync } from "node:fs";
27
28
  import { homedir } from "node:os";
28
29
  import { dirname, join, resolve } from "node:path";
@@ -30,6 +31,10 @@ import { createInterface } from "node:readline/promises";
30
31
  import { fileURLToPath } from "node:url";
31
32
  import qrcode from "qrcode-terminal";
32
33
  import { parseAllowList } from "./account-signin.js";
34
+ import { appendAdminAction, flushAdminActivity, sharedSignIn } from "./admin-activity.js";
35
+ import { bindDecisionRetention, decisionRetentionDays } from "./decision-log.js";
36
+ import { hostedWorkspaceConfigured } from "./enterprise.js";
37
+ import { resolveLoopbackTrust } from "./request-auth.js";
33
38
  import { writeFileAtomic } from "./atomic.js";
34
39
  import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
35
40
  import { runServiceCommand } from "./service-cli.js";
@@ -295,11 +300,23 @@ export function serverVersion(here = HERE) {
295
300
  }
296
301
  const message = (error) => (error instanceof Error ? error.message : String(error));
297
302
  // ── talking to a running server (loopback = owner) ────────────────────
303
+ /** Set only inside `openmausbot serve` on a service-trust server: the secret
304
+ * it handed the server it started, which opens that server's pairing route. */
305
+ let serveOwnerToken;
298
306
  async function api(port, path, init = {}) {
299
- const res = await fetch(`http://127.0.0.1:${port}${path}`, { method: init.method, body: init.body, headers: { "content-type": "application/json" }, signal: AbortSignal.timeout(3000) });
307
+ // x-openmausbot-cli names the tool in the admin activity log; on loopback
308
+ // it is the owner either way, so it grants nothing.
309
+ const headers = { "content-type": "application/json", "x-openmausbot-cli": "1", ...(serveOwnerToken ? { "x-openmausbot-cli-owner": serveOwnerToken } : {}) };
310
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, { method: init.method, body: init.body, headers, signal: AbortSignal.timeout(3000) });
300
311
  const body = await res.json().catch(() => ({}));
301
312
  return { status: res.status, body };
302
313
  }
314
+ /** What to do when a server treats this command as a local service rather
315
+ * than its owner (OMB_LOOPBACK_TRUST=service, or a hosted workspace). */
316
+ export const SERVICE_TRUST_HELP = "This server does not treat commands on this computer as its owner (OMB_LOOPBACK_TRUST=service, or a hosted workspace), so it will not pair devices or list sessions for them. Sign in as an admin and use Settings → Remote access, let people sign in with their email (openmausbot access add you@example.com), or restart the server with OMB_LOOPBACK_TRUST=owner.";
317
+ function refusedAsService(status, body) {
318
+ return status === 403 && typeof body?.error === "string" && /shared server|Sign in through the workspace portal/.test(body.error);
319
+ }
303
320
  async function serverUp(port, pid) {
304
321
  try {
305
322
  const { status, body } = await api(port, "/api/health");
@@ -472,6 +489,8 @@ async function mintPairing(port, options) {
472
489
  if (options.client)
473
490
  request.scopes = ["client"];
474
491
  const { status, body } = await api(port, "/api/auth/pairing", { method: "POST", body: JSON.stringify(request) });
492
+ if (refusedAsService(status, body))
493
+ throw new Error(SERVICE_TRUST_HELP);
475
494
  if (status !== 200)
476
495
  throw new Error(`server refused to mint a pairing code: ${typeof body?.error === "string" ? body.error : status}`);
477
496
  const url = options.publicUrl ? `${options.publicUrl}/pair#code=${body.code}` : typeof body.url === "string" ? body.url : null;
@@ -547,6 +566,10 @@ export async function runSessions(options) {
547
566
  }
548
567
  if (options.revoke) {
549
568
  const { status, body } = await api(options.port, `/api/auth/sessions/${encodeURIComponent(options.revoke)}`, { method: "DELETE" });
569
+ if (refusedAsService(status, body)) {
570
+ console.error(SERVICE_TRUST_HELP);
571
+ return 1;
572
+ }
550
573
  if (status !== 200) {
551
574
  console.error(`could not revoke: ${typeof body?.error === "string" ? body.error : status}`);
552
575
  return 1;
@@ -554,7 +577,15 @@ export async function runSessions(options) {
554
577
  console.log(`revoked ${options.revoke}: that device is signed out and its stream is closed`);
555
578
  return 0;
556
579
  }
557
- const { body } = await api(options.port, "/api/auth/sessions");
580
+ const { status, body } = await api(options.port, "/api/auth/sessions");
581
+ if (refusedAsService(status, body)) {
582
+ console.error(SERVICE_TRUST_HELP);
583
+ return 1;
584
+ }
585
+ if (status !== 200) {
586
+ console.error(`could not list sessions: ${typeof body?.error === "string" ? body.error : status}`);
587
+ return 1;
588
+ }
558
589
  const sessions = Array.isArray(body?.sessions) ? body.sessions : [];
559
590
  if (options.json) {
560
591
  console.log(JSON.stringify(sessions, null, 2));
@@ -623,9 +654,24 @@ export async function runAccess(options, io = defaultIo()) {
623
654
  const admins = list(Reflect.get(current, "admins"));
624
655
  const members = list(Reflect.get(current, "members"));
625
656
  const overridden = process.env.OMB_SIGNIN_EMAILS !== undefined || process.env.OMB_SIGNIN_MEMBER_EMAILS !== undefined;
626
- const write = (next) => {
657
+ const write = async (next) => {
627
658
  mkdirSync(options.dataDir, { recursive: true, mode: 0o700 });
628
659
  writeFileAtomic(file, `${JSON.stringify({ ...raw, signIn: next }, null, 2)}\n`, { mode: 0o600 });
660
+ // The same row Settings → People writes, named for the command line,
661
+ // pruned by the same window the server would use — kept, like the
662
+ // server's, only where more than one person signs in.
663
+ if (!sharedSignIn({ admins, members }) && !sharedSignIn(next) && !hostedWorkspaceConfigured())
664
+ return;
665
+ const decisions = raw.decisions && typeof raw.decisions === "object" ? raw.decisions.retentionDays : undefined;
666
+ bindDecisionRetention(() => decisionRetentionDays(typeof decisions === "number" ? decisions : undefined));
667
+ const changed = ["admins", "members"].filter((key) => next[key].join(",") !== (key === "admins" ? admins : members).join(","));
668
+ appendAdminAction(options.dataDir, {
669
+ category: "people", action: "people.update", target: { kind: "settings" }, actor: { kind: "cli" },
670
+ changed: changed.map((key) => `signIn.${key}`),
671
+ before: Object.fromEntries(changed.map((key) => [`signIn.${key}`, key === "admins" ? admins : members])),
672
+ after: Object.fromEntries(changed.map((key) => [`signIn.${key}`, next[key]])),
673
+ });
674
+ await flushAdminActivity(options.dataDir);
629
675
  };
630
676
  if (options.accessAction === "list") {
631
677
  if (!admins.length && !members.length) {
@@ -651,11 +697,11 @@ export async function runAccess(options, io = defaultIo()) {
651
697
  io.error(`${entry} is not on the list`);
652
698
  return 1;
653
699
  }
654
- write({ admins: without(admins), members: without(members) });
700
+ await write({ admins: without(admins), members: without(members) });
655
701
  io.log(`${entry} can no longer sign in (existing sessions stay until they expire or are revoked with \`openmausbot sessions revoke\`)`);
656
702
  return 0;
657
703
  }
658
- write(options.chatOnly ? { admins: without(admins), members: [...without(members), entry] } : { admins: [...without(admins), entry], members: without(members) });
704
+ await write(options.chatOnly ? { admins: without(admins), members: [...without(members), entry] } : { admins: [...without(admins), entry], members: without(members) });
659
705
  io.log(`${entry} can sign in at /pair with an emailed code (${options.chatOnly ? "chat and approvals" : "full access"})`);
660
706
  if (overridden)
661
707
  io.log("note: OMB_SIGNIN_EMAILS / OMB_SIGNIN_MEMBER_EMAILS are set in the environment and win over this list while the server runs");
@@ -883,6 +929,16 @@ export async function runServe(options, log = console.log) {
883
929
  };
884
930
  if (options.local)
885
931
  delete env.OMB_PUBLIC_URL;
932
+ // A service-trust server refuses session-less local admin requests, this
933
+ // CLI's included. Hand the server we start a per-launch secret over its
934
+ // stdin (not its environment, which every engine it starts inherits) so
935
+ // this process alone can still print the pairing code.
936
+ const serviceTrust = resolveLoopbackTrust({ env, desktopManaged: false, hostedWorkspace: hostedWorkspaceConfigured(env) }).trust === "service";
937
+ const ownerToken = serviceTrust ? randomBytes(32).toString("base64url") : undefined;
938
+ if (ownerToken)
939
+ env.OMB_CLI_OWNER_STDIN = "1";
940
+ else
941
+ delete env.OMB_CLI_OWNER_STDIN;
886
942
  if (entry.staticDir)
887
943
  env.OMB_STATIC_DIR = entry.staticDir;
888
944
  if (entry.skillsDir && !process.env.OMB_SKILLS_DIR)
@@ -925,7 +981,12 @@ export async function runServe(options, log = console.log) {
925
981
  throw new SetupCancelled();
926
982
  if (publicUrl)
927
983
  env.OMB_PUBLIC_URL = publicUrl;
928
- child = spawn(entry.command, entry.args, { env, stdio: ["ignore", logFd ?? "inherit", logFd ?? "inherit"] });
984
+ child = spawn(entry.command, entry.args, { env, stdio: [ownerToken ? "pipe" : "ignore", logFd ?? "inherit", logFd ?? "inherit"] });
985
+ if (ownerToken) {
986
+ child.stdin?.on("error", () => { });
987
+ child.stdin?.end(`${ownerToken}\n`);
988
+ serveOwnerToken = ownerToken;
989
+ }
929
990
  }
930
991
  catch (error) {
931
992
  if ((tailscaleServing || (startupCancelled && tailscaleAttempted)) && tailscale)
@@ -1048,13 +1109,20 @@ export async function runServe(options, log = console.log) {
1048
1109
  clearTimeout(timer);
1049
1110
  }
1050
1111
  if (!stopping && exited === null)
1051
- await showPhonePairing(options, publicUrl, log);
1112
+ await showPhonePairing(options, publicUrl, log).catch((error) => log(`no pairing code: ${message(error)}`));
1052
1113
  }
1053
1114
  else if (options.pair && !options.guided) {
1054
1115
  log("");
1055
- log(await mintPairing(options.port, { label: options.label ? `${options.label} owner` : undefined, client: options.client, publicUrl: publicUrl ?? undefined }));
1056
- log("");
1057
- log("another device later: openmausbot pair --label \"Kitchen iPad\"");
1116
+ // A refused code is no reason to stop a server that is running fine.
1117
+ try {
1118
+ log(await mintPairing(options.port, { label: options.label ? `${options.label} owner` : undefined, client: options.client, publicUrl: publicUrl ?? undefined }));
1119
+ log("");
1120
+ log("another device later: openmausbot pair --label \"Kitchen iPad\"");
1121
+ }
1122
+ catch (error) {
1123
+ log(`no pairing code: ${message(error)}`);
1124
+ log("start without one next time: openmausbot serve --no-pair");
1125
+ }
1058
1126
  }
1059
1127
  log(options.guided ? "\nKeep this terminal open while using your bots. Ctrl+C stops the server, not your saved work." : "stop with Ctrl+C");
1060
1128
  if (options.guided)
@@ -1062,6 +1130,7 @@ export async function runServe(options, log = console.log) {
1062
1130
  return await childExit;
1063
1131
  }
1064
1132
  finally {
1133
+ serveOwnerToken = undefined;
1065
1134
  await stop();
1066
1135
  process.removeListener("SIGINT", onSignal);
1067
1136
  process.removeListener("SIGTERM", onSignal);
@@ -895,7 +895,7 @@ export async function listToolkits(cfg) {
895
895
  ? backendFingerprint("managed-catalog", broker.url, broker.token)
896
896
  : null;
897
897
  if (identity && toolkitCache?.identity === identity && Date.now() - toolkitCache.at < 10 * 60_000) {
898
- return { cards: toolkitCache.cards, source: "api" };
898
+ return { cards: toolkitCache.cards, source: "api", pagination: toolkitCache.pagination };
899
899
  }
900
900
  if (backendKey || broker) {
901
901
  try {
@@ -907,6 +907,12 @@ export async function listToolkits(cfg) {
907
907
  const seenCursors = new Set();
908
908
  let cursor;
909
909
  let lastReportedPage;
910
+ let reportedTotalItems;
911
+ let reportedTotalPages;
912
+ // Why the walk ended. "limit" means the loop ran out of iterations;
913
+ // every early exit names itself so a stalled catalog can be logged
914
+ // instead of silently posing as the complete marketplace.
915
+ let stop = "limit";
910
916
  for (let page = 0; page < MAX_CONNECTED_ACCOUNT_PAGES; page += 1) {
911
917
  const params = new URLSearchParams({ limit: "500", sort_by: "usage" });
912
918
  if (cursor)
@@ -919,33 +925,51 @@ export async function listToolkits(cfg) {
919
925
  : await brokerRequest(cursor ? `/v1/catalog?cursor=${encodeURIComponent(cursor)}` : "/v1/catalog", {
920
926
  signal: AbortSignal.timeout(15_000),
921
927
  });
922
- if (!res.ok)
928
+ if (!res.ok) {
929
+ stop = "http-error";
923
930
  break;
931
+ }
924
932
  const json = await res.json();
925
933
  const pageItems = json.items ?? json.data ?? [];
926
- if (!Array.isArray(pageItems))
934
+ if (!Array.isArray(pageItems)) {
935
+ stop = "bad-page";
927
936
  break;
937
+ }
928
938
  items.push(...pageItems);
939
+ const pageTotalItems = Number(json.total_items);
940
+ if (Number.isFinite(pageTotalItems) && pageTotalItems > 0)
941
+ reportedTotalItems = pageTotalItems;
929
942
  // Composio reports current_page and total_pages beside next_cursor.
930
943
  // A broker that drops the cursor (or an upstream regression) can replay
931
944
  // a page while minting fresh cursors, so trust page movement: once it
932
945
  // stops advancing, the catalog is stuck and paging stops cleanly.
933
946
  const reportedPage = Number(json.current_page);
934
947
  if (Number.isFinite(reportedPage)) {
935
- if (lastReportedPage !== undefined && reportedPage <= lastReportedPage)
948
+ if (lastReportedPage !== undefined && reportedPage <= lastReportedPage) {
949
+ stop = "page-stuck";
936
950
  break;
951
+ }
937
952
  lastReportedPage = reportedPage;
938
953
  }
939
- const reportedTotalPages = Number(json.total_pages);
954
+ const pageTotalPages = Number(json.total_pages);
955
+ if (Number.isFinite(pageTotalPages) && pageTotalPages > 0)
956
+ reportedTotalPages = pageTotalPages;
940
957
  if (lastReportedPage !== undefined &&
941
- Number.isFinite(reportedTotalPages) &&
942
- reportedTotalPages > 0 &&
943
- lastReportedPage >= reportedTotalPages) {
958
+ Number.isFinite(pageTotalPages) &&
959
+ pageTotalPages > 0 &&
960
+ lastReportedPage >= pageTotalPages) {
961
+ stop = "total-reached";
944
962
  break;
945
963
  }
946
964
  const next = typeof json.next_cursor === "string" ? json.next_cursor.trim() : "";
947
- if (!next || seenCursors.has(next))
965
+ if (!next) {
966
+ stop = "end";
967
+ break;
968
+ }
969
+ if (seenCursors.has(next)) {
970
+ stop = "cursor-repeated";
948
971
  break;
972
+ }
949
973
  seenCursors.add(next);
950
974
  cursor = next;
951
975
  }
@@ -959,8 +983,22 @@ export async function listToolkits(cfg) {
959
983
  domain: null,
960
984
  }));
961
985
  const uniqueCards = cards.filter((card, index) => card.slug && cards.findIndex((candidate) => candidate.slug === card.slug) === index);
962
- toolkitCache = { at: Date.now(), cards: uniqueCards, identity: identity };
963
- return { cards: uniqueCards, source: "api" };
986
+ const shortOfReportedTotal = reportedTotalItems !== undefined && uniqueCards.length < reportedTotalItems;
987
+ // Trust reported page counts even when the walk ends "cleanly": an
988
+ // endpoint that stops at page 1 of 4 without a cursor is still partial.
989
+ const pagingStoppedEarly = lastReportedPage !== undefined
990
+ && reportedTotalPages !== undefined
991
+ && lastReportedPage < reportedTotalPages;
992
+ const pagination = { items: uniqueCards.length, stalled: shortOfReportedTotal || pagingStoppedEarly };
993
+ if (reportedTotalItems !== undefined)
994
+ pagination.totalItems = reportedTotalItems;
995
+ if (pagination.stalled) {
996
+ console.warn(`[composio] marketplace catalog paging stopped early (${stop}) after ${uniqueCards.length} toolkits`
997
+ + (reportedTotalItems !== undefined ? ` of ~${reportedTotalItems}` : "")
998
+ + "; serving a partial marketplace");
999
+ }
1000
+ toolkitCache = { at: Date.now(), cards: uniqueCards, identity: identity, pagination };
1001
+ return { cards: uniqueCards, source: "api", pagination };
964
1002
  }
965
1003
  }
966
1004
  catch {
@@ -294,6 +294,22 @@ const defaultModelSelectionSchema = z.object({
294
294
  effort: z.enum(EFFORT_LEVELS).optional(),
295
295
  variant: z.string().refine(isModelVariant, "invalid model variant").optional(),
296
296
  }).refine((selection) => selection.variant === undefined || selection.effort === undefined, "choose either a model variant or an effort level");
297
+ const threadsConfigSchema = z.object({
298
+ maxConcurrentPerBot: z.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS),
299
+ /** Cap each per-thread events/ and native/ NDJSON log at this many
300
+ * bytes; absent (the default) keeps today's unbounded growth (#1280). */
301
+ eventLogMaxBytes: z.number().int().min(MIN_THREAD_EVENT_LOG_BYTES).max(MAX_THREAD_EVENT_LOG_BYTES).optional(),
302
+ /** Days a closed or archived thread's event logs survive (#1280).
303
+ * Absent keeps them forever. */
304
+ eventLogRetentionDays: z.number().int().min(1).max(3650).optional(),
305
+ }).strict();
306
+ /** PATCH threads: every knob is independently patchable, and null clears an
307
+ * event-log knob back to its absent (off) default. */
308
+ const threadsPatchSchema = threadsConfigSchema.extend({
309
+ maxConcurrentPerBot: threadsConfigSchema.shape.maxConcurrentPerBot.optional(),
310
+ eventLogMaxBytes: threadsConfigSchema.shape.eventLogMaxBytes.nullable(),
311
+ eventLogRetentionDays: threadsConfigSchema.shape.eventLogRetentionDays.nullable(),
312
+ });
297
313
  const appConfigSchema = z.object({
298
314
  /** Verified by the dedicated domain endpoint, never a generic config patch. */
299
315
  customDomain: z.string().optional(),
@@ -395,15 +411,10 @@ const appConfigSchema = z.object({
395
411
  compactAt: z.number().positive().max(10_000_000).optional(),
396
412
  autoCompact: z.boolean().optional(),
397
413
  }).optional(),
398
- threads: z.object({
399
- maxConcurrentPerBot: z.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS),
400
- /** Cap each per-thread events/ and native/ NDJSON log at this many
401
- * bytes; absent (the default) keeps today's unbounded growth (#1280). */
402
- eventLogMaxBytes: z.number().int().min(MIN_THREAD_EVENT_LOG_BYTES).max(MAX_THREAD_EVENT_LOG_BYTES).optional(),
403
- /** Days a closed or archived thread's event logs survive (#1280).
404
- * Absent keeps them forever. */
405
- eventLogRetentionDays: z.number().int().min(1).max(3650).optional(),
406
- }).strict().optional(),
414
+ threads: threadsConfigSchema.optional(),
415
+ /** The authorization decision log (server/decision-log.ts): days of month
416
+ * files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
417
+ decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
407
418
  localVm: localVmConfigSchema.optional(),
408
419
  features: featureConfigSchema.optional(),
409
420
  onboarding: onboardingConfigSchema.optional(),
@@ -420,7 +431,8 @@ const appConfigSchema = z.object({
420
431
  const storedAppConfigSchema = appConfigSchema.extend({
421
432
  browserProfiles: storedBrowserProfilesSchema.optional(),
422
433
  });
423
- const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true });
434
+ const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
435
+ .extend({ threads: threadsPatchSchema.optional() });
424
436
  const jsonObjectSchema = z.record(z.string(), z.json());
425
437
  /** Resolve a canonical profile record to its exact durable Electron
426
438
  * partition identity. Callers must never substitute the display/API id. */
@@ -520,6 +532,8 @@ export function roomHandoffLimits(cfg) {
520
532
  hardCapMs: (cfg.rooms?.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES) * 60_000,
521
533
  };
522
534
  }
535
+ /** Accepts a full config or a parsed patch: the concurrency limit may be
536
+ * read from either, and a patch may legitimately omit it. */
523
537
  export function maxConcurrentBotThreads(cfg) {
524
538
  return cfg.threads?.maxConcurrentPerBot ?? DEFAULT_MAX_CONCURRENT_BOT_THREADS;
525
539
  }
@@ -776,6 +790,8 @@ export const WORKSPACE_CREDENTIAL_ENV = [
776
790
  "XAI_API_KEY",
777
791
  "OMB_ANTHROPIC_API_KEY",
778
792
  "OMB_ANTHROPIC_API_URL",
793
+ "OMB_HOSTED_MODEL_TOKEN",
794
+ "OMB_HOSTED_MODELS",
779
795
  "OPENAI_COMPAT_API_KEY",
780
796
  "OPENAI_COMPAT_URL",
781
797
  "BOX_TOKEN",
@@ -792,10 +808,32 @@ export const WORKSPACE_CREDENTIAL_ENV = [
792
808
  "OMB_BROWSER_CONNECTION",
793
809
  "OMB_USER_DATA",
794
810
  ];
795
- /** Drop every workspace credential from a child-process env (in place). */
811
+ /** Secrets of whoever operates this server, not of the workspace: the license
812
+ * key, a fleet container's installation credential, and everything a hosting
813
+ * control plane injects under `OMB_CLOUD_` (the readiness token, the bootstrap
814
+ * document and its gateway token). Only this process reads them. The prefix
815
+ * ends in an underscore on purpose: `OMB_CLOUDFLARED_PATH` is not one of them.
816
+ * What an engine is meant to receive arrives under another name through its
817
+ * instance environment (the hosted model token as ANTHROPIC_API_KEY or
818
+ * OPENMAUSBOT_COMPANY_API_KEY), so nothing here is ever an engine's input. */
819
+ export const CONTROL_PLANE_ENV = ["OMB_LICENSE_KEY", "OMB_INSTALLATION_CREDENTIAL"];
820
+ export const CONTROL_PLANE_ENV_PREFIX = "OMB_CLOUD_";
821
+ /** Drop every control-plane secret from a child-process env (in place). No
822
+ * driver allowlist re-admits these. Names compare case-insensitively because
823
+ * Windows environments do. */
824
+ export function stripControlPlaneEnv(env) {
825
+ for (const key of Object.keys(env)) {
826
+ const name = key.toUpperCase();
827
+ if (name.startsWith(CONTROL_PLANE_ENV_PREFIX) || CONTROL_PLANE_ENV.includes(name))
828
+ delete env[key];
829
+ }
830
+ }
831
+ /** Drop every workspace credential, and every control-plane secret, from a
832
+ * child-process env (in place). */
796
833
  export function stripWorkspaceCredentialEnv(env) {
797
834
  for (const key of WORKSPACE_CREDENTIAL_ENV)
798
835
  delete env[key];
836
+ stripControlPlaneEnv(env);
799
837
  }
800
838
  /** Env names a provider CLI might read as its own billing identity. A spawned
801
839
  * engine keeps only what its driver explicitly allows: a foreign key riding
@@ -815,6 +853,14 @@ export const PROVIDER_CREDENTIAL_ENV = [
815
853
  "CURSOR_API_KEY",
816
854
  "CURSOR_AUTH_TOKEN",
817
855
  ];
856
+ const configSaveListeners = new Set();
857
+ /** Told, synchronously, what each saveConfig wrote: the file before and
858
+ * after. The admin activity log (server/admin-activity.ts) records the
859
+ * change for whoever's request made it. A listener must not throw. */
860
+ export function onConfigSaved(listener) {
861
+ configSaveListeners.add(listener);
862
+ return () => { configSaveListeners.delete(listener); };
863
+ }
818
864
  /** Merge a partial config into ~/.openmausbot/config.json (secrets never
819
865
  * echoed back — callers report configured-or-not booleans only). */
820
866
  export function saveConfig(patch, options = {}) {
@@ -828,20 +874,32 @@ export function saveConfig(patch, options = {}) {
828
874
  catch {
829
875
  /* first write */
830
876
  }
831
- const checkedPatch = appConfigSchema.partial().parse(patch);
877
+ const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional() }).parse(patch);
878
+ const before = configSaveListeners.size ? structuredClone(disk) : disk;
832
879
  // A write is the durable migration point. Preserve every other raw key in
833
880
  // config.json, but never write #567's mixed-case or duplicate profile ids
834
881
  // back after we have successfully recognized the legacy list.
835
882
  const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
836
883
  if (storedProfiles.success)
837
884
  disk.browserProfiles = storedProfiles.data;
838
- for (const key of ["xai", "anthropic", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "onboarding", "browserEngine"]) {
885
+ for (const key of ["xai", "anthropic", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine"]) {
839
886
  const section = checkedPatch[key];
840
887
  if (!section)
841
888
  continue;
842
889
  const current = jsonObjectSchema.safeParse(disk[key]);
843
890
  const merged = current.success ? { ...current.data } : {};
891
+ // parseStoredConfig requires threads.maxConcurrentPerBot, so creating
892
+ // the section with only an event-log knob must still persist a valid
893
+ // concurrency default.
894
+ if (key === "threads" && !current.success)
895
+ merged.maxConcurrentPerBot = DEFAULT_MAX_CONCURRENT_BOT_THREADS;
844
896
  Object.assign(merged, section);
897
+ // null is the patch's explicit "remove this key" marker (today only the
898
+ // threads event-log knobs use it); a key the patch omits keeps its value.
899
+ for (const [sectionKey, sectionValue] of Object.entries(section)) {
900
+ if (sectionValue === null)
901
+ delete merged[sectionKey];
902
+ }
845
903
  disk[key] = merged;
846
904
  }
847
905
  if (checkedPatch.vps !== undefined)
@@ -920,6 +978,14 @@ export function saveConfig(patch, options = {}) {
920
978
  }
921
979
  mkdirSync(DATA_DIR, { recursive: true });
922
980
  writeFileAtomic(p, JSON.stringify(disk, null, 2), { mode: 0o600 });
981
+ for (const listener of configSaveListeners) {
982
+ try {
983
+ listener(before, disk);
984
+ }
985
+ catch (error) {
986
+ console.warn(`config: a save listener failed: ${error instanceof Error ? error.message : String(error)}`);
987
+ }
988
+ }
923
989
  }
924
990
  /** Set one instance's `config.cli` ("" clears the override back to the
925
991
  * driver default). Creating the instance entry is fine — a config-less