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
@@ -269,7 +269,26 @@ export function validateLinuxDescriptorRuntime(descriptorFile, raw, { uid = proc
269
269
  return false;
270
270
  }
271
271
  }
272
- export function readCuaConnection({ platform = process.platform, userData = process.env.OMB_USER_DATA, home = homedir(), validateLinuxRuntime = validateLinuxDescriptorRuntime, } = {}) {
272
+ /** Legacy darwin/win32 descriptors still name any command, so at runtime at
273
+ * least the file itself must be one only this user could have written: a
274
+ * regular file, not a symlink, owned by this user, and closed against
275
+ * group/other writes. libuv reports every writable Windows file as uid 0
276
+ * mode 0o666, so the ownership and permission bits cannot carry meaning
277
+ * there — only the symlink refusal applies on win32. */
278
+ function validateLegacyDescriptorRuntime(descriptorFile, platform, { uid = process.getuid?.() ?? -1 } = {}) {
279
+ try {
280
+ const stat = lstatSync(descriptorFile);
281
+ if (!stat.isFile() || stat.isSymbolicLink())
282
+ return false;
283
+ if (platform !== "win32" && (stat.uid !== uid || (stat.mode & 0o022) !== 0))
284
+ return false;
285
+ return true;
286
+ }
287
+ catch {
288
+ return false;
289
+ }
290
+ }
291
+ export function readCuaConnection({ platform = process.platform, userData = process.env.OMB_USER_DATA, home = homedir(), validateLinuxRuntime = validateLinuxDescriptorRuntime, validateLegacyRuntime = validateLegacyDescriptorRuntime, } = {}) {
273
292
  const candidates = userData ? [join(userData, "cua-connection.json")] : [];
274
293
  if (platform === "darwin") {
275
294
  // Legacy/dev fallback. Packaged Electron passes its exact userData path.
@@ -287,7 +306,7 @@ export function readCuaConnection({ platform = process.platform, userData = proc
287
306
  }
288
307
  else {
289
308
  const decoded = decodeLegacyDescriptor(raw, platform);
290
- if (decoded)
309
+ if (decoded && validateLegacyRuntime(file, platform))
291
310
  return decoded;
292
311
  }
293
312
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstatSync, mkdirSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
5
  const uuid = z.string().uuid();
@@ -9,9 +9,18 @@ const connectionSchema = z.object({
9
9
  email: z.string().email().max(320), deviceId: uuid, token: z.string().regex(/^omg_[A-Za-z0-9_-]{43}$/),
10
10
  expiresAt: z.number().finite().int().positive(),
11
11
  providers: z.array(z.object({ id: z.enum(["anthropic", "openai", "openrouter"]), configured: z.boolean(), models: z.array(model).max(500) }).strict()).max(3),
12
+ /** The Admin is up but its operator licence lapsed: keep the instances
13
+ * listed as unavailable instead of treating it as a revocation. */
14
+ suspended: z.literal("license-expired").optional(),
12
15
  }).strict();
16
+ /** The saved enrollment's identity (never its token), sent before it is
17
+ * cleared or when it has already expired, so references to its old
18
+ * device-scoped ids still move to the stable ids. */
19
+ const identitySchema = z.object({ portalOrigin: z.string().max(2048), organizationId: uuid, email: z.string().email().max(320), deviceId: uuid }).strict();
20
+ const PROVIDER_IDS = ["anthropic", "openai", "openrouter"];
13
21
  const signature = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
14
22
  const expiredMessage = "Company access has ended. Reconnect your organization or explicitly choose a personal model; personal billing will not be used automatically.";
23
+ export const LICENSE_EXPIRED_MESSAGE = "Your organisation's OpenMaus Admin licence has expired. Contact your admin.";
15
24
  /** Validate only messages received over Electron's private utility-parent port.
16
25
  * Never expose this input to renderer HTTP or merge it into saved AppConfig.
17
26
  */
@@ -33,10 +42,18 @@ export function parseManagedDesktopConnection(raw, now = Date.now()) {
33
42
  return { ...value, portalOrigin: url.origin, email: value.email.toLowerCase(), providers: value.providers.map(provider => ({ ...provider, models: [...new Set(provider.models)] })).sort((a, b) => a.id.localeCompare(b.id)) };
34
43
  }
35
44
  export function companyInstanceId(connection, provider) {
36
- // Device identity prevents an old company's native resume cursor from being
37
- // inherited by a different enrollment using the same organization/email.
45
+ // Stable for one person in one organisation, so re-enrolling this computer
46
+ // keeps bots' Company model choices and native resume. Another account or
47
+ // organisation still gets separate ids, homes and cursors.
48
+ return `company.${signature([connection.portalOrigin, connection.organizationId, connection.email.toLowerCase()]).slice(0, 24)}.${provider}`;
49
+ }
50
+ /** The pre-0.1.86 id, which also hashed the enrollment's deviceId. */
51
+ export function legacyCompanyInstanceId(connection, provider) {
38
52
  return `company.${signature([connection.portalOrigin, connection.organizationId, connection.deviceId]).slice(0, 24)}.${provider}`;
39
53
  }
54
+ /** Everything that decides which native instances exist and what they hold.
55
+ * Expiry and licence suspension change availability only, never a process. */
56
+ const instanceSignature = (connection) => signature(connection && (({ expiresAt: _expiresAt, suspended: _suspended, ...identity }) => identity)(connection));
40
57
  export function companyInstanceConfigs(connection, runtimeDirectory) {
41
58
  const entries = {};
42
59
  for (const provider of connection.providers) {
@@ -86,9 +103,18 @@ export class ManagedDesktopProviders {
86
103
  known = new Set();
87
104
  owns(instanceId) { return this.known.has(instanceId); }
88
105
  apply(raw, force = false) {
89
- const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature = signature(connection);
90
- if (!force && nextSignature === this.currentSignature)
106
+ const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature = instanceSignature(connection);
107
+ if (!force && nextSignature === this.currentSignature) {
108
+ // A renewal or licence change: same processes, new availability.
109
+ const previous = this.connection;
110
+ this.connection = connection;
111
+ if (connection && previous && (connection.expiresAt !== previous.expiresAt || connection.suspended !== previous.suspended)) {
112
+ clearTimeout(this.timer);
113
+ this.scheduleExpiry(this.revision);
114
+ this.options.onAvailability?.();
115
+ }
91
116
  return this.tail;
117
+ }
92
118
  this.currentSignature = nextSignature;
93
119
  this.connection = connection;
94
120
  const revision = ++this.revision;
@@ -107,6 +133,7 @@ export class ManagedDesktopProviders {
107
133
  // workspace backups. Device-scoped ids prevent a new enrollment from
108
134
  // inheriting another account's native conversation or credentials.
109
135
  const directory = this.ensureDirectory(["providers", "company"]);
136
+ await this.migrateLegacyIds(connection);
110
137
  const configs = companyInstanceConfigs(connection, directory);
111
138
  for (const [id, entry] of Object.entries(configs)) {
112
139
  if (this.options.registry.entries().some(existing => existing.instanceId === id))
@@ -117,7 +144,9 @@ export class ManagedDesktopProviders {
117
144
  this.ids.add(id);
118
145
  this.known.add(id);
119
146
  }
120
- const active = () => revision === this.revision && connection.expiresAt > this.now();
147
+ // Read the current grant: renewals and licence changes update it in place.
148
+ const active = () => revision === this.revision && Boolean(this.connection && this.connection.expiresAt > this.now() && !this.connection.suspended);
149
+ const unavailable = () => this.connection?.suspended === "license-expired" ? LICENSE_EXPIRED_MESSAGE : expiredMessage;
121
150
  await this.options.registry.load(configs, instance => {
122
151
  const provider = connection.providers.find(provider => companyInstanceId(connection, provider.id) === instance.instanceId);
123
152
  const models = { default: provider.models[0], options: provider.models.map(id => ({ id, label: `${id} (Company)` })) };
@@ -127,17 +156,17 @@ export class ManagedDesktopProviders {
127
156
  startAuthentication: undefined, getAuthentication: undefined, completeAuthentication: undefined, cancelAuthentication: undefined, signOut: undefined,
128
157
  snapshot: async () => {
129
158
  if (!active())
130
- return { state: "unavailable", reason: expiredMessage };
159
+ return { state: "unavailable", reason: unavailable() };
131
160
  const snapshot = await instance.snapshot();
132
161
  if (!active())
133
- return { state: "unavailable", reason: expiredMessage };
162
+ return { state: "unavailable", reason: unavailable() };
134
163
  return { ...snapshot, authenticated: snapshot.state === "available", billing: "metered", account: { email: connection.email, organization: connection.organizationName, method: "api-key" } };
135
164
  },
136
165
  adapter: {
137
166
  ...instance.adapter,
138
167
  sendTurn: async (input) => {
139
168
  if (!active())
140
- throw new Error(expiredMessage);
169
+ throw new Error(unavailable());
141
170
  if (!input.model || !provider.models.includes(input.model))
142
171
  throw new Error("This model is not enabled by your Company administrator.");
143
172
  return instance.adapter.sendTurn(input);
@@ -147,7 +176,7 @@ export class ManagedDesktopProviders {
147
176
  });
148
177
  if (revision === this.revision) {
149
178
  this.options.afterReplace?.([...this.ids]);
150
- this.scheduleExpiry(connection, revision);
179
+ this.scheduleExpiry(revision);
151
180
  }
152
181
  };
153
182
  this.tail = this.tail.catch(() => { }).then(replace).catch(async (error) => {
@@ -188,17 +217,53 @@ export class ManagedDesktopProviders {
188
217
  }
189
218
  return directory;
190
219
  }
191
- scheduleExpiry(connection, revision) {
220
+ scheduleExpiry(revision) {
221
+ const expiresAt = this.connection?.expiresAt;
222
+ if (expiresAt === undefined)
223
+ return;
192
224
  this.timer = setTimeout(() => {
193
225
  if (revision !== this.revision)
194
226
  return;
195
- if (connection.expiresAt > this.now())
196
- this.scheduleExpiry(connection, revision);
227
+ if ((this.connection?.expiresAt ?? 0) > this.now())
228
+ this.scheduleExpiry(revision);
197
229
  else
198
230
  void this.apply(null).catch(() => { });
199
- }, Math.min(2_147_483_647, Math.max(1, connection.expiresAt - this.now())));
231
+ }, Math.min(2_147_483_647, Math.max(1, expiresAt - this.now())));
200
232
  this.timer.unref?.();
201
233
  }
234
+ /** Moves an enrollment's old device-scoped ids to the stable ids: native
235
+ * homes are renamed only when the stable one does not exist yet, then saved
236
+ * references follow. Best effort: a failure is logged and never keeps
237
+ * Company models down. */
238
+ async migrateLegacyIds(identity) {
239
+ try {
240
+ const directory = this.ensureDirectory(["providers", "company"]);
241
+ const aliases = PROVIDER_IDS.map(provider => ({ from: legacyCompanyInstanceId(identity, provider), to: companyInstanceId(identity, provider) }));
242
+ for (const { from, to } of aliases) {
243
+ const legacy = join(directory, from), stable = join(directory, to);
244
+ if (!existsSync(legacy) || existsSync(stable))
245
+ continue;
246
+ const stat = lstatSync(legacy);
247
+ if (stat.isDirectory() && !stat.isSymbolicLink())
248
+ renameSync(legacy, stable);
249
+ }
250
+ await this.options.migrate?.(aliases);
251
+ }
252
+ catch (error) {
253
+ console.warn(`[company] could not move saved Company model references to their stable ids: ${error instanceof Error ? error.message : String(error)}`);
254
+ }
255
+ }
256
+ /** Migrates a saved enrollment that is being cleared or has expired. */
257
+ async migrateIdentity(raw) {
258
+ const parsed = identitySchema.parse(raw), url = new URL(parsed.portalOrigin);
259
+ if (url.origin !== parsed.portalOrigin || (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) {
260
+ throw new Error("Company identity must use an HTTPS origin or loopback fixture.");
261
+ }
262
+ const identity = { ...parsed, email: parsed.email.toLowerCase() };
263
+ const migration = this.tail.catch(() => { }).then(() => this.migrateLegacyIds(identity));
264
+ this.tail = migration;
265
+ return migration;
266
+ }
202
267
  async close() {
203
268
  await this.apply(null, true);
204
269
  }
@@ -0,0 +1,153 @@
1
+ import { z } from "zod";
2
+ /** An organisation's desktop policy, received only over Electron's private
3
+ * utility-parent port while this desktop is enrolled. It lives in memory: it
4
+ * is never merged into config.json, and it adds no prompts or approvals —
5
+ * a disallowed action is simply refused with a sentence saying who manages it. */
6
+ const policySchema = z.object({
7
+ organizationId: z.string().uuid(),
8
+ organizationName: z.string().trim().min(1).max(100),
9
+ expiresAt: z.number().int().positive(),
10
+ version: z.number().int().min(0).max(2_147_483_647),
11
+ companyModelsOnly: z.boolean(),
12
+ allowedEngines: z.union([z.literal("all"), z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,63}$/)).max(64)]),
13
+ mcp: z.object({ allowCustom: z.boolean(), allowlist: z.array(z.string().trim().min(1).max(200)).max(100) }).strict(),
14
+ computers: z.object({ thisComputer: z.boolean(), localVm: z.boolean(), box: z.boolean(), vps: z.boolean() }).strict(),
15
+ remoteAccess: z.boolean(),
16
+ }).strict();
17
+ const computerLabels = { thisComputer: "this computer", localVm: "local virtual machines", box: "Box cloud computers", vps: "VPS computers" };
18
+ export function parseManagedPolicy(raw, now = Date.now()) {
19
+ if (raw === null)
20
+ return null;
21
+ const parsed = policySchema.safeParse(raw);
22
+ if (!parsed.success)
23
+ throw new Error("Invalid organisation policy from the desktop parent.");
24
+ return parsed.data.expiresAt > now ? parsed.data : null;
25
+ }
26
+ /** `*` in a host pattern stands for one or more whole labels, never part of
27
+ * one, so `*.example.com` cannot match `evil.test/x.example.com`. */
28
+ function hostMatches(pattern, host) {
29
+ if (!pattern.length)
30
+ return !host.length;
31
+ if (pattern[0] === "*") {
32
+ for (let taken = 1; taken <= host.length - (pattern.length - 1); taken++)
33
+ if (hostMatches(pattern.slice(1), host.slice(taken)))
34
+ return true;
35
+ return false;
36
+ }
37
+ return host.length > 0 && pattern[0] === host[0] && hostMatches(pattern.slice(1), host.slice(1));
38
+ }
39
+ /** A name entry matches the configured server name. An address entry
40
+ * (https:// only) is parsed like the server's URL: no credentials, the same
41
+ * port, the host compared label by label, and the path separately (`*` there
42
+ * matches any characters). Anything that does not parse never matches. */
43
+ export function mcpEntryMatches(entry, name, url) {
44
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(entry))
45
+ return entry.toLowerCase() === name.toLowerCase();
46
+ if (!url)
47
+ return false;
48
+ let pattern, target;
49
+ try {
50
+ pattern = new URL(entry);
51
+ target = new URL(url);
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ if (pattern.protocol !== "https:" || target.protocol !== "https:" || pattern.username || pattern.password || target.username || target.password ||
57
+ pattern.search || pattern.hash || pattern.port !== target.port)
58
+ return false;
59
+ const labels = pattern.hostname.split(".");
60
+ if (labels.some(label => !label || (label !== "*" && label.includes("*"))) || labels.every(label => label === "*"))
61
+ return false;
62
+ if (!hostMatches(labels, target.hostname.split(".")))
63
+ return false;
64
+ const path = pattern.pathname.split("*").map(part => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
65
+ return new RegExp(`^${path}$`).test(target.pathname);
66
+ }
67
+ /** A resource key claimed by bindTurnComputer, mapped to the policy's kind. */
68
+ export function computerKindForResource(resource) {
69
+ if (resource === "computer:host")
70
+ return "thisComputer";
71
+ if (resource.startsWith("computer:vm:"))
72
+ return "localVm";
73
+ if (resource.startsWith("computer:box:") || resource.startsWith("computer:box-bot:"))
74
+ return "box";
75
+ if (resource.startsWith("computer:vps:"))
76
+ return "vps";
77
+ }
78
+ export class ManagedDesktopPolicy {
79
+ policy = null;
80
+ timer;
81
+ now;
82
+ onChange;
83
+ constructor(options = {}) {
84
+ this.now = options.now ?? Date.now;
85
+ this.onChange = options.onChange ?? (() => { });
86
+ }
87
+ apply(raw) {
88
+ const next = parseManagedPolicy(raw, this.now());
89
+ const changed = JSON.stringify(next) !== JSON.stringify(this.policy);
90
+ this.policy = next;
91
+ clearTimeout(this.timer);
92
+ // The last applied policy holds only as long as the device credential.
93
+ if (next) {
94
+ this.timer = setTimeout(() => this.apply(null), Math.min(2_147_483_647, Math.max(1, next.expiresAt - this.now())));
95
+ this.timer.unref?.();
96
+ }
97
+ if (changed)
98
+ this.onChange();
99
+ }
100
+ current() {
101
+ return this.policy && this.policy.expiresAt > this.now() ? this.policy : null;
102
+ }
103
+ close() { clearTimeout(this.timer); this.policy = null; }
104
+ /** Why this organisation does not allow an instance to run a turn, or undefined. */
105
+ modelRefusal(instance, company) {
106
+ const policy = this.current();
107
+ if (!policy)
108
+ return;
109
+ if (policy.companyModelsOnly && !company)
110
+ return `${policy.organizationName} allows only company models on this computer. Choose a Company model for this bot.`;
111
+ if (policy.allowedEngines !== "all" && !policy.allowedEngines.includes(instance.driverKind)) {
112
+ return `${policy.organizationName} does not allow the ${instance.displayName ?? instance.driverKind} engine on this computer. Choose another model for this bot.`;
113
+ }
114
+ }
115
+ mcpAllowed(name, url) {
116
+ const policy = this.current();
117
+ return !policy || policy.mcp.allowCustom || policy.mcp.allowlist.some(entry => mcpEntryMatches(entry, name, url));
118
+ }
119
+ /** Only approved servers reach engines while custom servers are off. */
120
+ filterMcp(servers) {
121
+ if (!this.restrictsMcp())
122
+ return servers;
123
+ return Object.fromEntries(Object.entries(servers).filter(([name, server]) => this.mcpAllowed(name, "url" in server && typeof server.url === "string" ? server.url : undefined)));
124
+ }
125
+ restrictsMcp() { const policy = this.current(); return Boolean(policy && !policy.mcp.allowCustom); }
126
+ mcpRefusal(name, url) {
127
+ const policy = this.current();
128
+ if (!policy || this.mcpAllowed(name, url))
129
+ return;
130
+ return `${policy.organizationName} allows only MCP servers it has approved. Ask your administrator to add this server to the approved list.`;
131
+ }
132
+ computerAllowed(kind) { return this.current()?.computers[kind] ?? true; }
133
+ computerRefusal(kind) {
134
+ const policy = this.current();
135
+ if (!policy || policy.computers[kind])
136
+ return;
137
+ return `${policy.organizationName} does not allow bots to use ${computerLabels[kind]}.`;
138
+ }
139
+ remoteAccessRefusal() {
140
+ const policy = this.current();
141
+ if (!policy || policy.remoteAccess)
142
+ return;
143
+ return `${policy.organizationName} does not allow remote access to this computer.`;
144
+ }
145
+ /** Read-only view for the renderer; carries nothing secret. */
146
+ summary() {
147
+ const policy = this.current();
148
+ if (!policy)
149
+ return null;
150
+ const { organizationId: _id, expiresAt: _expires, ...visible } = policy;
151
+ return visible;
152
+ }
153
+ }
@@ -1,6 +1,6 @@
1
1
  // The shared stdio bridge for the host computer, Local VM, and BYO VPS.
2
2
  //
3
- // It is almost transparent — bytes in, bytes out — with two deliberate
3
+ // It is almost transparent — bytes in, bytes out — with three deliberate
4
4
  // near-side exceptions:
5
5
  //
6
6
  // 1. `ping`. The bundled cua-driver (through at least v0.22.1) does not
@@ -9,6 +9,11 @@
9
9
  // 2. The who-is-driving `gate` (opt-in via `gate`). While the person holds
10
10
  // control of this computer, a `tools/call` from the agent is answered
11
11
  // with a refusal here and never forwarded.
12
+ // 3. `tools/list` schemas. Every engine passes a tool's inputSchema to its
13
+ // model provider, and strict providers refuse a root that is not a plain
14
+ // object — failing the whole turn. The answer to a tools/list is
15
+ // rewritten to a provider-safe root (see mcp-tool-schema.ts); the far
16
+ // end still validates each call against its own schema.
12
17
  //
13
18
  // Both behaviors live here so neither entry point can drift:
14
19
  // 1. Exit without truncation. `process.exit()` in a close/error handler
@@ -23,6 +28,7 @@ import { spawn } from "node:child_process";
23
28
  import { StringDecoder } from "node:string_decoder";
24
29
  import { CONTROL_REFUSAL_PLAIN, createControlClient } from "./control-client.js";
25
30
  import { augmentedPath } from "./env-path.js";
31
+ import { createToolListNormalizer } from "./mcp-tool-schema.js";
26
32
  // 45s of TOTAL silence before the bridge even probes. An MCP session is
27
33
  // legitimately quiet between tool calls and a slow screenshot can take tens
28
34
  // of seconds, so silence alone never kills anything — it only triggers a
@@ -228,8 +234,10 @@ export function runMcpBridge(options) {
228
234
  }
229
235
  : {}),
230
236
  });
237
+ const toolLists = createToolListNormalizer();
231
238
  let pendingInput = Promise.resolve();
232
239
  const inbound = createLineSplitter((line) => {
240
+ toolLists.observeRequest(line);
233
241
  const completion = intercept(line);
234
242
  if (completion)
235
243
  pendingInput = completion;
@@ -245,7 +253,7 @@ export function runMcpBridge(options) {
245
253
  // Injected responses and refusals must never land inside one of the
246
254
  // child's half-written frames, so the child's stdout is re-emitted at
247
255
  // line granularity as well.
248
- const outbound = createLineSplitter((line) => process.stdout.write(line + "\n"));
256
+ const outbound = createLineSplitter((line) => process.stdout.write(toolLists.rewriteResponse(line) + "\n"));
249
257
  child.stdout.on("data", (chunk) => outbound.push(chunk));
250
258
  child.stdout.on("end", () => outbound.flush());
251
259
  const detach = () => {
@@ -0,0 +1,37 @@
1
+ const asMcpImage = (c) => !!c &&
2
+ typeof c === "object" &&
3
+ c.type === "image" &&
4
+ typeof c.data === "string" &&
5
+ typeof c.mimeType === "string"
6
+ ? c
7
+ : null;
8
+ // Claude CLI reformats MCP image content into Anthropic Messages API shape
9
+ // before drivers ever see it — {type:"image", source:{type:"base64",
10
+ // media_type, data}} — rather than the MCP-native {type, data, mimeType}.
11
+ // Other drivers (codex/acp) pass the MCP-native shape through unchanged.
12
+ const asAnthropicImage = (c) => {
13
+ const source = c?.type === "image"
14
+ ? c.source
15
+ : null;
16
+ if (!source || typeof source !== "object")
17
+ return null;
18
+ const { type, media_type, data } = source;
19
+ return type === "base64" && typeof media_type === "string" && typeof data === "string"
20
+ ? { data, mimeType: media_type }
21
+ : null;
22
+ };
23
+ const asImageBlock = (c) => asMcpImage(c) ?? asAnthropicImage(c);
24
+ /** MCP tool_result content mixes text/image blocks. toolDetailPreview redacts
25
+ * the image bytes for the activity-log line (by design — that preview is
26
+ * never meant to carry raw base64); this pulls the images out separately so
27
+ * a caller can fold them into the message as attachments instead of losing
28
+ * them. Accepts either a raw MCP content array or a `{ content: [...] }`
29
+ * wrapper, since drivers hand this differently-shaped payloads. */
30
+ export function extractMcpImages(value) {
31
+ const arr = Array.isArray(value)
32
+ ? value
33
+ : Array.isArray(value?.content)
34
+ ? value.content
35
+ : null;
36
+ return arr ? arr.map(asImageBlock).filter((v) => v !== null) : [];
37
+ }
@@ -0,0 +1,108 @@
1
+ const ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf", "not"];
2
+ function isObject(value) {
3
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function branches(value) {
6
+ return Array.isArray(value) ? value.filter(isObject) : [];
7
+ }
8
+ /** The object shape a schema implies, gathered through its combinators.
9
+ * Its own declarations win over a branch's narrower copy; a property that
10
+ * only a branch declares is kept so the model still knows it exists. A name
11
+ * is required when the schema or an allOf branch requires it, or when every
12
+ * anyOf/oneOf choice does — a requirement of only one choice is not. */
13
+ function objectShape(schema) {
14
+ const properties = isObject(schema.properties) ? { ...schema.properties } : {};
15
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((key) => typeof key === "string") : []);
16
+ const adopt = (shape) => {
17
+ for (const [key, value] of Object.entries(shape.properties))
18
+ if (!(key in properties))
19
+ properties[key] = value;
20
+ };
21
+ for (const branch of branches(schema.allOf)) {
22
+ const shape = objectShape(branch);
23
+ adopt(shape);
24
+ for (const key of shape.required)
25
+ required.add(key);
26
+ }
27
+ for (const keyword of ["anyOf", "oneOf"]) {
28
+ const choices = branches(schema[keyword]).map(objectShape);
29
+ if (choices.length === 0)
30
+ continue;
31
+ for (const choice of choices)
32
+ adopt(choice);
33
+ for (const key of choices[0].required) {
34
+ if (choices.every((choice) => choice.required.includes(key)))
35
+ required.add(key);
36
+ }
37
+ }
38
+ return { properties, required: [...required] };
39
+ }
40
+ function describesObject(type) {
41
+ return type === undefined || type === "object" || (Array.isArray(type) && type.includes("object"));
42
+ }
43
+ /** A strict-provider-safe version of an MCP tool's inputSchema. A schema
44
+ * that is already safe is returned as the same object, so callers can tell
45
+ * whether anything changed. The input is never mutated. */
46
+ export function providerSafeInputSchema(schema) {
47
+ if (!isObject(schema) || !describesObject(schema.type))
48
+ return { type: "object", properties: {} };
49
+ if (schema.type === "object" && ROOT_COMBINATORS.every((keyword) => !(keyword in schema)))
50
+ return schema;
51
+ const { properties, required } = objectShape(schema);
52
+ const safe = { ...schema, type: "object", properties };
53
+ for (const keyword of ROOT_COMBINATORS)
54
+ delete safe[keyword];
55
+ if (required.length > 0)
56
+ safe.required = required;
57
+ else
58
+ delete safe.required;
59
+ return safe;
60
+ }
61
+ /** Rewrites tools/list responses on an MCP stdio stream. Only a response to
62
+ * a tools/list the agent actually sent is touched; every other line — tool
63
+ * results, notifications, errors, non-JSON — passes through byte-for-byte. */
64
+ export function createToolListNormalizer() {
65
+ // JSON-RPC ids may be numbers or strings, and 7 is not "7".
66
+ const pending = new Set();
67
+ const idKey = (id) => `${typeof id}:${String(id)}`;
68
+ return {
69
+ observeRequest(line) {
70
+ let frame;
71
+ try {
72
+ frame = JSON.parse(line);
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ if (isObject(frame) && frame.method === "tools/list" && (typeof frame.id === "number" || typeof frame.id === "string")) {
78
+ pending.add(idKey(frame.id));
79
+ }
80
+ },
81
+ rewriteResponse(line) {
82
+ if (pending.size === 0)
83
+ return line;
84
+ let frame;
85
+ try {
86
+ frame = JSON.parse(line);
87
+ }
88
+ catch {
89
+ return line;
90
+ }
91
+ if (!isObject(frame) || "method" in frame || !pending.delete(idKey(frame.id)))
92
+ return line;
93
+ if (!isObject(frame.result) || !Array.isArray(frame.result.tools))
94
+ return line;
95
+ let changed = false;
96
+ const tools = frame.result.tools.map((tool) => {
97
+ if (!isObject(tool))
98
+ return tool;
99
+ const inputSchema = providerSafeInputSchema(tool.inputSchema);
100
+ if (inputSchema === tool.inputSchema)
101
+ return tool;
102
+ changed = true;
103
+ return { ...tool, inputSchema };
104
+ });
105
+ return changed ? JSON.stringify({ ...frame, result: { ...frame.result, tools } }) : line;
106
+ },
107
+ };
108
+ }