openmausbot 0.1.84 → 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 (230) 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-DNa2umw-.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/companion/src/routes.js +1 -0
  125. package/dist-server/container-mcp.js +231 -17
  126. package/dist-server/drivers/agents-proxy.js +16097 -1411
  127. package/dist-server/enterprise/LICENSE +32 -0
  128. package/dist-server/enterprise/server/index.js +6 -5
  129. package/dist-server/hooks/omb-hook.js +74 -0
  130. package/dist-server/index.js +12495 -7584
  131. package/dist-server/local-computer-proxy.js +83 -1
  132. package/dist-server/local-computer.js +14 -2
  133. package/dist-server/mcp-server.js +3 -1
  134. package/dist-server/openmausbot.js +5458 -4587
  135. package/dist-server/pair-cli.js +5458 -4587
  136. package/dist-server/proxy-paths.js +1 -0
  137. package/dist-server/server/admin-activity.js +410 -0
  138. package/dist-server/server/agent-tool-policy.js +1 -0
  139. package/dist-server/server/bot-package.js +2 -0
  140. package/dist-server/server/bot-visibility.js +462 -0
  141. package/dist-server/server/box.js +25 -5
  142. package/dist-server/server/browser-engine.js +87 -10
  143. package/dist-server/server/browser-live.js +7 -5
  144. package/dist-server/server/browser-runtime.js +66 -11
  145. package/dist-server/server/channel-queue.js +2 -0
  146. package/dist-server/server/checkpoints.js +62 -18
  147. package/dist-server/server/cli-prompts.js +26 -2
  148. package/dist-server/server/cli.js +79 -10
  149. package/dist-server/server/commands.js +27 -0
  150. package/dist-server/server/compaction-summary.js +78 -0
  151. package/dist-server/server/composio.js +49 -11
  152. package/dist-server/server/config.js +137 -14
  153. package/dist-server/server/context-budget.js +29 -0
  154. package/dist-server/server/context-rebuild.js +72 -0
  155. package/dist-server/server/decision-log.js +209 -48
  156. package/dist-server/server/default-model-selection.js +20 -3
  157. package/dist-server/server/delegations.js +11 -9
  158. package/dist-server/server/digest.js +130 -0
  159. package/dist-server/server/drivers/acp/core.js +637 -238
  160. package/dist-server/server/drivers/acp/grok.js +4 -3
  161. package/dist-server/server/drivers/agents-call.js +1123 -0
  162. package/dist-server/server/drivers/agents-catalog.js +659 -0
  163. package/dist-server/server/drivers/agents-client.js +28 -0
  164. package/dist-server/server/drivers/agents-proxy.js +18 -1683
  165. package/dist-server/server/drivers/agents-result.js +23 -0
  166. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  167. package/dist-server/server/drivers/claude.js +83 -16
  168. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  169. package/dist-server/server/drivers/codex.js +32 -4
  170. package/dist-server/server/drivers/grok.js +2 -1
  171. package/dist-server/server/drivers/openai-chat.js +66 -45
  172. package/dist-server/server/drivers/openai-compat.js +16 -8
  173. package/dist-server/server/enterprise.js +97 -16
  174. package/dist-server/server/external-runtime.js +83 -0
  175. package/dist-server/server/guarded-requests.js +60 -0
  176. package/dist-server/server/harness/http.js +61 -0
  177. package/dist-server/server/hooks/omb-hook.js +103 -0
  178. package/dist-server/server/hosted-models.js +151 -0
  179. package/dist-server/server/hosted-slack.js +11 -0
  180. package/dist-server/server/index.js +2810 -678
  181. package/dist-server/server/local-computer.js +21 -2
  182. package/dist-server/server/managed-desktop.js +79 -14
  183. package/dist-server/server/managed-policy.js +153 -0
  184. package/dist-server/server/mcp-bridge.js +10 -2
  185. package/dist-server/server/mcp-tool-images.js +37 -0
  186. package/dist-server/server/mcp-tool-schema.js +108 -0
  187. package/dist-server/server/message-db.js +108 -21
  188. package/dist-server/server/message-file.js +3 -2
  189. package/dist-server/server/model-context-window.js +7 -0
  190. package/dist-server/server/model-prices.js +121 -0
  191. package/dist-server/server/notify.js +10 -1
  192. package/dist-server/server/package-export.js +1 -0
  193. package/dist-server/server/peer-roster.js +17 -4
  194. package/dist-server/server/provider-icon.js +20 -0
  195. package/dist-server/server/proxy-paths.js +1 -0
  196. package/dist-server/server/recent-work.js +15 -0
  197. package/dist-server/server/request-auth.js +97 -0
  198. package/dist-server/server/room-handoffs.js +41 -13
  199. package/dist-server/server/routes/hosted-slack.js +20 -0
  200. package/dist-server/server/routes/table.js +14 -0
  201. package/dist-server/server/routine-requests.js +39 -1
  202. package/dist-server/server/routines.js +65 -9
  203. package/dist-server/server/shared-computer-control.js +31 -4
  204. package/dist-server/server/spend.js +111 -16
  205. package/dist-server/server/steer-queue.js +12 -1
  206. package/dist-server/server/store.js +384 -33
  207. package/dist-server/server/system-prompt.js +4 -4
  208. package/dist-server/server/team-backup.js +19 -2
  209. package/dist-server/server/thread-starters.js +53 -0
  210. package/dist-server/server/tool-results.js +72 -0
  211. package/dist-server/server/tts/grok.js +74 -0
  212. package/dist-server/server/tts/index.js +19 -1
  213. package/dist-server/server/usage-ledger.js +31 -8
  214. package/dist-server/server/webhooks.js +28 -0
  215. package/dist-server/server/workspace-backup-policy.js +9 -2
  216. package/dist-server/server/workspace-backup.js +7 -4
  217. package/dist-server/shared/approval-mode.js +7 -1
  218. package/dist-server/shared/digest.js +1 -0
  219. package/dist-server/shared/markdown-windows-paths.js +36 -0
  220. package/dist-server/shared/mention-boundary.js +15 -0
  221. package/dist-server/shared/provider-icon.js +110 -0
  222. package/dist-server/shared/routines.js +7 -1
  223. package/dist-server/shared/team-backup.js +5 -0
  224. package/dist-server/src/lib/composer-attachments.js +16 -0
  225. package/dist-server/vps-container-mcp.js +236 -22
  226. package/enterprise/README.md +33 -12
  227. package/enterprise/server/index.js +6 -5
  228. package/package.json +1 -1
  229. package/dist/assets/index-BbU5REzd.js +0 -310
  230. package/dist/assets/index-CLGfYlx_.css +0 -1
@@ -28,9 +28,12 @@ function decodeConfig(raw) {
28
28
  const config = (raw ?? {});
29
29
  if (config.tools !== undefined && typeof config.tools !== "boolean")
30
30
  throw new Error("tools must be a boolean");
31
+ if (config.managedModels !== undefined && (!Array.isArray(config.managedModels) || !config.managedModels.length || config.managedModels.some(model => typeof model !== "string" || !model.trim())))
32
+ throw new Error("Invalid managed models.");
31
33
  const envUrl = process.env.OPENAI_COMPAT_URL;
32
34
  return {
33
35
  ...(config.tools !== undefined ? { tools: config.tools } : {}),
36
+ ...(config.managedModels ? { managedModels: config.managedModels } : {}),
34
37
  url: (typeof config.url === "string" && config.url ? config.url : envUrl || "https://openrouter.ai/api/v1")
35
38
  .replace(/\/+$/, ""),
36
39
  apiKeyEnv: typeof config.apiKeyEnv === "string" && config.apiKeyEnv
@@ -74,15 +77,19 @@ export const OpenAICompatDriver = {
74
77
  process.env[config.apiKeyEnv] ??
75
78
  process.env.OPENAI_COMPAT_API_KEY ??
76
79
  "";
77
- let catalog = config.model
78
- ? {
79
- default: config.model,
80
- options: DEFAULT_MODELS.options.some((model) => model.id === config.model)
81
- ? DEFAULT_MODELS.options
82
- : [{ id: config.model, label: config.model, custom: true }, ...DEFAULT_MODELS.options],
83
- }
84
- : DEFAULT_MODELS;
80
+ let catalog = config.managedModels
81
+ ? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) }
82
+ : config.model
83
+ ? {
84
+ default: config.model,
85
+ options: DEFAULT_MODELS.options.some((model) => model.id === config.model)
86
+ ? DEFAULT_MODELS.options
87
+ : [{ id: config.model, label: config.model, custom: true }, ...DEFAULT_MODELS.options],
88
+ }
89
+ : DEFAULT_MODELS;
85
90
  const fetchModels = async () => {
91
+ if (config.managedModels)
92
+ return;
86
93
  if (!apiKey)
87
94
  return;
88
95
  try {
@@ -132,6 +139,7 @@ export const OpenAICompatDriver = {
132
139
  model,
133
140
  messages,
134
141
  stream,
142
+ stream_options: stream ? { include_usage: true } : undefined,
135
143
  ...(config.provider && isOpenRouterUrl(config.url)
136
144
  ? { provider: { order: [config.provider], allow_fallbacks: false } }
137
145
  : {}),
@@ -8,10 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  // Neutral hook point for the source-available enterprise layer.
10
10
  //
11
- // The layer lives in ../enterprise (its own LICENSE) and is loaded only if that
11
+ // The layer lives in enterprise/ (its own LICENSE) and is loaded only if that
12
12
  // folder is present: delete it and this file still compiles, the server still
13
13
  // starts, and /api/edition reports the open-source edition. Core never imports
14
- // the layer statically, so the bundle and the packaged app carry no trace of it.
14
+ // the layer statically; a build ships it as its own bundled file beside the
15
+ // server (see enterpriseLayerDirs for where it is looked up).
15
16
  // The layer's only obligation is `register()`, which turns OMB_LICENSE_KEY into
16
17
  // entitlements; core keeps the resulting status and answers `entitled()`.
17
18
  import { existsSync } from "node:fs";
@@ -25,7 +26,28 @@ const layerSchema = z.object({
25
26
  features: z.array(z.string().min(1)),
26
27
  expiresAt: z.string().nullable(),
27
28
  });
28
- const DEFAULT_DIR = join(SERVER_ROOT, "..", "enterprise");
29
+ /** Days before expiry that the startup log, /api/edition and Settings start
30
+ * saying so, and days after it that the features keep working while the
31
+ * operator renews. The key and its verification are unchanged: the layer
32
+ * accepts a key that expired less than the grace period ago. */
33
+ export const LICENSE_WARN_DAYS = 30;
34
+ export const LICENSE_GRACE_DAYS = 7;
35
+ const DAY_MS = 24 * 60 * 60_000;
36
+ /** Where the layer may be, in order. An explicit OMB_ENTERPRISE_DIR is the
37
+ * only place looked at when set. Otherwise: beside the server root (a
38
+ * checkout, where server/ and enterprise/ are siblings, and the npm package,
39
+ * which copies the bundled layer to <package>/enterprise), then inside it
40
+ * (the Docker image and the packaged desktop ship dist-server/ alone, and
41
+ * scripts/bundle-server.mjs writes the layer to dist-server/enterprise). */
42
+ export function enterpriseLayerDirs(env = process.env, serverRoot = SERVER_ROOT) {
43
+ if (env.OMB_ENTERPRISE_DIR)
44
+ return [env.OMB_ENTERPRISE_DIR];
45
+ return [join(serverRoot, "..", "enterprise"), join(serverRoot, "enterprise")];
46
+ }
47
+ /** Source in a checkout, a compiled bundle in an image: same convention as proxy-paths.ts. */
48
+ function layerEntry(dir) {
49
+ return [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
50
+ }
29
51
  let current = { edition: "oss", features: [] };
30
52
  let workspaceAccessFactory;
31
53
  export function hostedWorkspaceConfigured(env = process.env) {
@@ -53,6 +75,26 @@ export function hostedWorkspaceConfiguration(env = process.env) {
53
75
  return null;
54
76
  }
55
77
  }
78
+ /** Who decides who may use this workspace, for Settings → People and
79
+ * Remote access. On a portal-membership workspace the organisation's Admin
80
+ * does, and `peopleUrl` opens its People page for this workspace (identifiers
81
+ * only; Admin authorizes its own visitor). A hosted workspace never issues
82
+ * pairing codes or email sign-in: people come in through the portal. */
83
+ export function workspaceMembership(env = process.env) {
84
+ const hosted = hostedWorkspaceConfiguration(env);
85
+ const pairingCodes = !hostedWorkspaceConfigured(env);
86
+ if (!hosted?.portalMembership)
87
+ return { authority: "local", pairingCodes };
88
+ const people = new URL("/people", hosted.admin);
89
+ people.search = new URLSearchParams({ workspace: hosted.workspace }).toString();
90
+ return { authority: "portal", pairingCodes, peopleUrl: people.href };
91
+ }
92
+ /** An operator may authorize new Full tasks only on a dedicated,
93
+ * portal-managed server. This is not an HTTP setting or a desktop grant. */
94
+ export function sharedWorkspaceFullAccessConfigured(env = process.env) {
95
+ return env.OMB_SHARED_WORKSPACE_FULL_ACCESS === "1" && env.OMB_DESKTOP_PARENT !== "1" &&
96
+ hostedWorkspaceConfiguration(env)?.portalMembership === true;
97
+ }
56
98
  /** No enterprise import or portal dependency enters the core bundle. A
57
99
  * configured server without this hook must refuse hosted access, not fall
58
100
  * back to legacy email or QR credentials. */
@@ -69,12 +111,11 @@ function oss(notice) {
69
111
  * degrades to the open-source edition with a notice that says what to fix. */
70
112
  export async function loadEnterpriseLayer(options = {}) {
71
113
  workspaceAccessFactory = undefined;
72
- const dir = options.dir ?? process.env.OMB_ENTERPRISE_DIR ?? DEFAULT_DIR;
114
+ const dirs = options.dir ? [options.dir] : enterpriseLayerDirs(options.env, options.serverRoot);
73
115
  const licenseKey = options.licenseKey ?? process.env.OMB_LICENSE_KEY;
74
- // Source in a checkout, a compiled bundle in an image: same convention as proxy-paths.ts.
75
- const entry = [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
116
+ const entry = dirs.map(layerEntry).find((candidate) => candidate !== undefined);
76
117
  if (!entry) {
77
- return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${dir}` : undefined);
118
+ return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${dirs.join(" or ")}` : undefined);
78
119
  }
79
120
  try {
80
121
  const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
@@ -86,7 +127,9 @@ export async function loadEnterpriseLayer(options = {}) {
86
127
  throw new Error(`${entry} does not export register()`);
87
128
  if (!licenseKey)
88
129
  return oss("enterprise layer present but OMB_LICENSE_KEY is not set");
89
- const registered = await register({ licenseKey });
130
+ // graceDays is additive: a layer that ignores it refuses an expired key
131
+ // at startup exactly as before.
132
+ const registered = await register({ licenseKey, graceDays: LICENSE_GRACE_DAYS });
90
133
  const layer = layerSchema.parse(registered);
91
134
  current = {
92
135
  edition: "enterprise",
@@ -100,27 +143,65 @@ export async function loadEnterpriseLayer(options = {}) {
100
143
  return oss(`enterprise layer disabled: ${error instanceof Error ? error.message : String(error)}`);
101
144
  }
102
145
  }
146
+ /** When the key stops being valid, or null for perpetual / open-source. */
147
+ function expiryAt() {
148
+ if (current.edition !== "enterprise" || !current.expiresAt)
149
+ return null;
150
+ const at = new Date(current.expiresAt).getTime();
151
+ return Number.isFinite(at) ? at : null;
152
+ }
103
153
  /** A license that expires while the server keeps running stops granting
104
- * features at that moment, not at the next restart. */
154
+ * features once its grace period ends, at that moment, not at the next restart. */
105
155
  function expired(now) {
106
- if (current.edition !== "enterprise" || !current.expiresAt)
107
- return false;
108
- return now >= new Date(current.expiresAt).getTime();
156
+ const at = expiryAt();
157
+ return at !== null && now >= at + LICENSE_GRACE_DAYS * DAY_MS;
109
158
  }
110
159
  export function editionStatus(now = Date.now()) {
111
- if (!expired(now))
160
+ const at = expiryAt();
161
+ if (at === null)
112
162
  return current;
163
+ if (expired(now)) {
164
+ return {
165
+ edition: "oss",
166
+ features: [],
167
+ notice: `enterprise layer disabled: OMB_LICENSE_KEY expired on ${current.expiresAt}; renew it to keep enterprise features`,
168
+ };
169
+ }
170
+ const expiresInDays = Math.ceil((at - now) / DAY_MS);
171
+ if (now < at)
172
+ return { ...current, expiresInDays };
173
+ const graceEndsAt = new Date(at + LICENSE_GRACE_DAYS * DAY_MS).toISOString().slice(0, 10);
113
174
  return {
114
- edition: "oss",
115
- features: [],
116
- notice: `enterprise layer disabled: OMB_LICENSE_KEY expired on ${current.expiresAt}; renew it to keep enterprise features`,
175
+ ...current,
176
+ expiresInDays,
177
+ graceEndsAt,
178
+ notice: `OMB_LICENSE_KEY expired on ${current.expiresAt}; enterprise features keep working until ${graceEndsAt} while it is renewed`,
117
179
  };
118
180
  }
181
+ /** The edition as a non-admin session sees it: what is entitled, without
182
+ * the license's countdown, grace date or operator notice. */
183
+ export function editionForMembers(status) {
184
+ const { expiresInDays: _days, graceEndsAt: _grace, notice: _notice, ...shared } = status;
185
+ return shared;
186
+ }
119
187
  /** Feature gates in core ask this and nothing else. Unknown ids are simply not
120
188
  * granted, and the open-source edition always carries an empty feature list. */
121
189
  export function entitled(feature, now = Date.now()) {
122
190
  return !expired(now) && current.features.includes(feature);
123
191
  }
192
+ /** The startup warning for a key that is about to lapse or already in its
193
+ * grace period; null when there is nothing to say. */
194
+ export function licenseWarning(status) {
195
+ if (status.edition !== "enterprise" || !status.expiresAt || status.expiresInDays === undefined)
196
+ return null;
197
+ if (status.graceEndsAt) {
198
+ return `OMB_LICENSE_KEY expired on ${status.expiresAt}; enterprise features keep working until ${status.graceEndsAt} — renew the key before then`;
199
+ }
200
+ if (status.expiresInDays > LICENSE_WARN_DAYS)
201
+ return null;
202
+ const days = status.expiresInDays === 1 ? "1 day" : `${status.expiresInDays} days`;
203
+ return `OMB_LICENSE_KEY expires on ${status.expiresAt} (in ${days}); renew it before then to keep enterprise features`;
204
+ }
124
205
  /** One line for the startup log. */
125
206
  export function describeEdition(status) {
126
207
  if (status.edition === "enterprise") {
@@ -0,0 +1,83 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
3
+ const MAX_FILE_BYTES = 1_048_576;
4
+ const ID = /^[\w-]{1,128}$/;
5
+ const fingerprint = (token) => createHash("sha256").update(token).digest("hex");
6
+ const sameFingerprint = (left, right) => {
7
+ const a = Buffer.from(left);
8
+ const b = Buffer.from(right);
9
+ return a.length === b.length && timingSafeEqual(a, b);
10
+ };
11
+ /** Read a bounded regular file through the descriptor whose permissions we
12
+ * checked. A pipe must not block the request loop, nor a pathname swap replace
13
+ * the checked file before the read. */
14
+ function registrations(file) {
15
+ let fd;
16
+ try {
17
+ fd = openSync(file, constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK));
18
+ const stat = fstatSync(fd);
19
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
20
+ return [];
21
+ if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
22
+ console.warn("[comms] ignoring external runtime credentials: use a private file (chmod 600)");
23
+ return [];
24
+ }
25
+ const bytes = Buffer.alloc(stat.size + 1);
26
+ let count = 0;
27
+ for (;;) {
28
+ const read = readSync(fd, bytes, count, bytes.length - count, null);
29
+ if (!read)
30
+ break;
31
+ count += read;
32
+ if (count > stat.size)
33
+ return []; // changed while being read
34
+ }
35
+ const parsed = JSON.parse(bytes.subarray(0, count).toString("utf8"));
36
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
37
+ return [];
38
+ return Object.entries(parsed).flatMap(([botId, value]) => {
39
+ if (!ID.test(botId) || !value || typeof value !== "object" || Array.isArray(value))
40
+ return [];
41
+ const { token, threadId } = value;
42
+ if (typeof token !== "string" || token.length < 32 || token.length > 4096 || /\s/.test(token))
43
+ return [];
44
+ if (typeof threadId !== "string" || !ID.test(threadId))
45
+ return [];
46
+ return [{ botId, tokenHash: fingerprint(token), threadId }];
47
+ });
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ finally {
53
+ if (fd !== undefined)
54
+ closeSync(fd);
55
+ }
56
+ }
57
+ function resolveRegistration(file, tokenHash, botForId) {
58
+ const matches = registrations(file).filter(entry => sameFingerprint(entry.tokenHash, tokenHash));
59
+ // A copied secret must not select a different bot based on JSON key order.
60
+ if (matches.length !== 1)
61
+ return null;
62
+ const registration = matches[0];
63
+ const bot = botForId(registration.botId);
64
+ if (!bot || bot.hidden)
65
+ return null;
66
+ // There is no durable "main thread": bot.threadId is the UI selection and
67
+ // new tasks are prepended. Every grant needs an explicit binding.
68
+ const task = bot.tasks?.find(candidate => candidate.threadId === registration.threadId);
69
+ if (!task || task.archivedAt !== undefined)
70
+ return null;
71
+ return { botId: bot.id, threadId: task.threadId, tokenHash };
72
+ }
73
+ export function authorizeExternalRuntime(file, header, botForId) {
74
+ if (typeof header !== "string" || !/^Bearer \S{32,4096}$/.test(header))
75
+ return null;
76
+ return resolveRegistration(file, fingerprint(header.slice(7)), botForId);
77
+ }
78
+ /** Re-check after every body/approval/poll wait. The captured identity is not
79
+ * a lease: token rotation, rebinding, deletion and archive take effect now. */
80
+ export function externalRuntimeIsActive(file, grant, botForId) {
81
+ const current = resolveRegistration(file, grant.tokenHash, botForId);
82
+ return current?.botId === grant.botId && current.threadId === grant.threadId;
83
+ }
@@ -0,0 +1,60 @@
1
+ export function requestConflict(message = "The request no longer owns this conversation") {
2
+ return Object.assign(new Error(message), { status: 409, code: "guarded_request_changed" });
3
+ }
4
+ export function assertRequestTarget(current, expected) {
5
+ if (current.messageId !== expected.messageId || current.activeLeafId !== expected.expectedActiveLeafId ||
6
+ current.activeTurnId !== expected.expectedTurnId || current.executionId !== expected.expectedExecutionId)
7
+ throw requestConflict();
8
+ }
9
+ /** Never infer ownership from timestamps or the latest assistant text. */
10
+ export function guardedRequestPath(messages, activeLeafId, sendId) {
11
+ const sources = messages.filter(message => message.sendId === sendId);
12
+ const source = sources[0];
13
+ if (sources.length !== 1 || !source || source.role !== "user" || source.kind !== "text" || source.steered || source.peerAsk) {
14
+ throw requestConflict("No unique acknowledged user request matches this sendId");
15
+ }
16
+ const byId = new Map(messages.map(message => [message.id, message]));
17
+ const path = [];
18
+ const seen = new Set();
19
+ let id = activeLeafId;
20
+ while (id) {
21
+ if (seen.has(id))
22
+ throw requestConflict();
23
+ seen.add(id);
24
+ const message = byId.get(id);
25
+ if (!message)
26
+ throw requestConflict();
27
+ path.unshift(message);
28
+ if (message.id === source.id)
29
+ break;
30
+ if (path.length >= 500)
31
+ throw Object.assign(new Error("Open this request in the workspace; its transcript exceeds the response limit"), { status: 413 });
32
+ id = message.parentId ?? null;
33
+ }
34
+ if (path[0]?.id !== source.id || path.slice(1).some(message => message.role === "user"))
35
+ throw requestConflict();
36
+ // An abandoned sibling can hold a detached result or approval. A caller
37
+ // must never receive it as the answer to the current branch.
38
+ const ancestors = new Set(path.map(message => message.id));
39
+ if (messages.some(message => message.parentId && ancestors.has(message.parentId) && !seen.has(message.id)))
40
+ throw requestConflict();
41
+ return path;
42
+ }
43
+ /** Used only for an explicit control-plane card, never the latest user. */
44
+ export function requestSourceForCard(messages, messageId) {
45
+ const byId = new Map(messages.map(message => [message.id, message]));
46
+ const seen = new Set();
47
+ let message = byId.get(messageId);
48
+ while (message && !seen.has(message.id)) {
49
+ seen.add(message.id);
50
+ if (message.role === "user")
51
+ return message.peerAsk || message.steered || message.requestCancelled ? undefined : message.id;
52
+ message = message.parentId ? byId.get(message.parentId) : undefined;
53
+ }
54
+ return undefined;
55
+ }
56
+ export function requestNeedsInput(message) {
57
+ return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed) ||
58
+ (message.connector && !message.connector.dismissed && !message.connector.resumed && message.connector.status !== "connected") ||
59
+ (message.secret && !message.secret.provided && !message.secret.dismissed));
60
+ }
@@ -0,0 +1,61 @@
1
+ const jsonHooks = new WeakMap();
2
+ const parsedBodies = new WeakMap();
3
+ /** Pass every JSON body sent on this response through `hook` first (it may
4
+ * return a narrowed copy). Set once a request is authenticated: index.ts
5
+ * narrows what a member is sent and notes what an admin change answered.
6
+ * Hooks added later run after earlier ones. */
7
+ export function onJsonBody(res, hook) {
8
+ const previous = jsonHooks.get(res);
9
+ jsonHooks.set(res, previous ? (body) => hook(previous(body)) : hook);
10
+ }
11
+ /** The JSON body readBody parsed for this request, if it read one. */
12
+ export function parsedBodyOf(req) {
13
+ return parsedBodies.get(req);
14
+ }
15
+ export function json(res, status, body) {
16
+ const hook = jsonHooks.get(res);
17
+ const data = JSON.stringify(hook ? hook(body) : body);
18
+ res.writeHead(status, { "content-type": "application/json" });
19
+ res.end(data);
20
+ }
21
+ export function readBody(req, limit = 1_000_000) {
22
+ return new Promise((resolve, reject) => {
23
+ let data = "";
24
+ let bytes = 0;
25
+ let done = false;
26
+ const fail = (status, msg) => {
27
+ if (done)
28
+ return;
29
+ done = true;
30
+ const err = Object.assign(new Error(msg), { status });
31
+ reject(err);
32
+ };
33
+ req.on("data", (c) => {
34
+ if (done)
35
+ return;
36
+ bytes += typeof c === "string" ? Buffer.byteLength(c) : c.length;
37
+ if (bytes > limit) {
38
+ // Keep draining the socket, but stop retaining attacker-controlled
39
+ // bytes. Destroying the request here prevents the caller from
40
+ // receiving the useful 413 response.
41
+ return fail(413, "body too large");
42
+ }
43
+ data += c;
44
+ });
45
+ req.on("end", () => {
46
+ if (done)
47
+ return;
48
+ let body;
49
+ try {
50
+ body = data ? JSON.parse(data) : {};
51
+ }
52
+ catch {
53
+ return fail(400, "invalid JSON body");
54
+ }
55
+ done = true;
56
+ parsedBodies.set(req, body);
57
+ resolve(body);
58
+ });
59
+ req.on("error", (e) => fail(400, e instanceof Error ? e.message : String(e)));
60
+ });
61
+ }
@@ -0,0 +1,103 @@
1
+ // omb-hook — the one command OpenMausBot registers for Claude Code hooks
2
+ // (PostToolUse, PreCompact, SessionStart, Stop). Claude Code runs it with the
3
+ // hook's JSON on stdin and waits for it, so it obeys three rules learned
4
+ // the hard way by other harnesses:
5
+ //
6
+ // 1. It ALWAYS exits 0. A hook that fails or times out can block the
7
+ // agent's next step; the harness observing is never worth that.
8
+ // 2. It has a hard budget (OMB_HOOK_TIMEOUT_MS, default 4000) below Claude
9
+ // Code's own per-hook timeout, and speaks only to the loopback harness.
10
+ // 3. It is dependency-free and does no work itself: it forwards the event
11
+ // to POST /api/internal/hook with the turn's capability token (read
12
+ // from OMB_HOOK_TOKEN_FILE at run time — the token rotates per turn
13
+ // while the CLI process, and its environment, live on) and prints the
14
+ // harness's `hookSpecificOutput`, if any, for Claude Code to apply.
15
+ //
16
+ // stdout is the hook channel — never console.log anything else here.
17
+ import { readFileSync } from "node:fs";
18
+ const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit"]);
19
+ const requestedBudget = Number(process.env.OMB_HOOK_TIMEOUT_MS);
20
+ const budgetMs = Number.isFinite(requestedBudget) && requestedBudget > 0 ? Math.min(requestedBudget, 4_000) : 4_000;
21
+ const MAX_INPUT_BYTES = 1024 * 1024;
22
+ const url = process.env.OMB_HOOK_URL ?? "";
23
+ const tokenFile = process.env.OMB_HOOK_TOKEN_FILE ?? "";
24
+ const done = (out) => {
25
+ if (out === undefined)
26
+ process.exit(0);
27
+ process.stdout.write(out, () => process.exit(0));
28
+ };
29
+ process.stdout.on("error", () => process.exit(0));
30
+ // the outer fuse: whatever is still pending when this fires, we leave
31
+ const fuse = setTimeout(() => done(), budgetMs);
32
+ fuse.unref();
33
+ function readStdin() {
34
+ return new Promise((resolve) => {
35
+ let data = "";
36
+ let bytes = 0;
37
+ process.stdin.setEncoding("utf8");
38
+ process.stdin.on("data", (chunk) => {
39
+ bytes += Buffer.byteLength(chunk);
40
+ if (bytes > MAX_INPUT_BYTES)
41
+ return done();
42
+ data += chunk;
43
+ });
44
+ process.stdin.on("end", () => resolve(data));
45
+ process.stdin.on("error", () => resolve(data));
46
+ });
47
+ }
48
+ async function main() {
49
+ const raw = await readStdin();
50
+ let payload;
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
54
+ return done();
55
+ payload = parsed;
56
+ }
57
+ catch {
58
+ return done();
59
+ }
60
+ const event = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
61
+ if (!event || !/^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(url) || !tokenFile)
62
+ return done();
63
+ let token = "";
64
+ try {
65
+ token = readFileSync(tokenFile, "utf8").trim();
66
+ }
67
+ catch {
68
+ return done();
69
+ }
70
+ if (!token)
71
+ return done();
72
+ const controller = new AbortController();
73
+ const timer = setTimeout(() => controller.abort(), Math.max(200, budgetMs - 300));
74
+ try {
75
+ const response = await fetch(`${url}/api/internal/hook`, {
76
+ method: "POST",
77
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
78
+ body: JSON.stringify({ event, payload }),
79
+ redirect: "error",
80
+ signal: controller.signal,
81
+ });
82
+ if (!response.ok)
83
+ return done();
84
+ const body = (await response.json().catch(() => null));
85
+ if (body && typeof body === "object" && body.hookSpecificOutput && typeof body.hookSpecificOutput === "object") {
86
+ return done(JSON.stringify({ hookSpecificOutput: body.hookSpecificOutput }));
87
+ }
88
+ // On SessionStart (and UserPromptSubmit) Claude Code reads plain-text
89
+ // stdout as context the model sees; on every other event stdout is
90
+ // only a debug line, so a context body is printed nowhere else.
91
+ if (body && typeof body === "object" && typeof body.context === "string" && body.context && CONTEXT_EVENTS.has(event)) {
92
+ return done(body.context);
93
+ }
94
+ return done();
95
+ }
96
+ catch {
97
+ return done();
98
+ }
99
+ finally {
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+ void main();