openmausbot 0.1.86 → 0.1.88

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 (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -0,0 +1,165 @@
1
+ // Advertisement filtering for the connector MCP bridge (issue #1737, part
2
+ // of the per-bot Composio tool grants umbrella #1734).
3
+ //
4
+ // Call-time enforcement (connector-verdict.ts, slice 2) stays the
5
+ // authority: it judges the real tool names on the harness relay endpoint,
6
+ // where the loopback token already names the bot. This module is surface
7
+ // only — it trims a tools/list response to the tools the bot was granted,
8
+ // so the model is never offered a call that would be refused a moment
9
+ // later. The allowlist reaches the bridge as JSON in the environment, the
10
+ // same channel every other bridge input uses; whenever filtering cannot
11
+ // run — no env var, malformed env, oversized payload — the bridge relays
12
+ // the list untouched and the grants stay enforced at call time.
13
+ import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
14
+ import { serviceSlugFor, serviceSlugForCandidates } from "./connector-verdict.js";
15
+ /** The env var mcpIntegration sets and connector-proxy.ts reads. */
16
+ export const CONNECTOR_ALLOWED_TOOLS_ENV = "OMB_CONNECTOR_ALLOWED_TOOLS";
17
+ /** The env var carrying the connected-service slugs the filter resolves
18
+ * tool-name prefixes against. Without it the filter falls back to the
19
+ * plain first-segment split, exactly as when the catalog is unreachable. */
20
+ export const CONNECTOR_SERVICE_SLUGS_ENV = "OMB_CONNECTOR_SERVICE_SLUGS";
21
+ /** Upper bound for the serialized allowlist. Past it the env var is
22
+ * omitted entirely (mounting must never depend on catalog size) and the
23
+ * bot keeps the unfiltered list with call-time enforcement only. */
24
+ export const CONNECTOR_ALLOWED_TOOLS_MAX_BYTES = 32 * 1024;
25
+ /** Upper bound for the serialized slug list; past it the env var is
26
+ * omitted and the filter falls back to the plain split. */
27
+ export const CONNECTOR_SERVICE_SLUGS_MAX_BYTES = 32 * 1024;
28
+ /** The platform meta-tools. They name no connected-app tool themselves —
29
+ * search and schemas discover, the executor runs what it is told — so
30
+ * they stay advertised whenever any tool is granted; the executor's
31
+ * arguments are still judged call-time by connector-verdict.ts. */
32
+ export const CONNECTOR_META_TOOLS = [
33
+ "COMPOSIO_SEARCH_TOOLS",
34
+ "COMPOSIO_GET_TOOL_SCHEMAS",
35
+ "COMPOSIO_MULTI_EXECUTE_TOOL",
36
+ // Session-level connection meta-tools. COMPOSIO_MANAGE_CONNECTIONS ships
37
+ // in every Composio session; COMPOSIO_WAIT_FOR_CONNECTIONS is opt-in and
38
+ // off by default. They prefix as service "composio", which no grant
39
+ // names, so they ride this list instead of CONNECTION_TOOL_SUFFIXES —
40
+ // connector-proxy intercepts their calls to surface connection cards.
41
+ // The filter only keeps names upstream actually lists, so advertising an
42
+ // opted-out tool changes nothing.
43
+ "COMPOSIO_MANAGE_CONNECTIONS",
44
+ "COMPOSIO_WAIT_FOR_CONNECTIONS",
45
+ ];
46
+ const CONNECTION_TOOL_SUFFIXES = ["_MANAGE_CONNECTIONS", "_WAIT_FOR_CONNECTIONS"];
47
+ /** Harness side: serialize a grants record for the bridge env, or report
48
+ * that it exceeded the cap (the caller warns and omits it). */
49
+ export function serializeConnectorAllowedTools(grants) {
50
+ const env = JSON.stringify(grants);
51
+ if (Buffer.byteLength(env, "utf8") > CONNECTOR_ALLOWED_TOOLS_MAX_BYTES)
52
+ return { oversized: true };
53
+ return { env, oversized: false };
54
+ }
55
+ /** Harness side: serialize the connected-service slugs for the bridge
56
+ * env, or report that they exceeded the cap. Duplicate or non-slug
57
+ * entries are dropped rather than failing the whole mount. */
58
+ export function serializeConnectorServiceSlugs(slugs) {
59
+ const unique = [...new Set(slugs.filter((slug) => CONNECTOR_SLUG_PATTERN.test(slug)))];
60
+ if (!unique.length)
61
+ return { oversized: false };
62
+ const env = JSON.stringify(unique);
63
+ if (Buffer.byteLength(env, "utf8") > CONNECTOR_SERVICE_SLUGS_MAX_BYTES)
64
+ return { oversized: true };
65
+ return { env, oversized: false };
66
+ }
67
+ /** Bridge side: decode and shape-check the allowlist env. Anything off —
68
+ * absent, unparseable, wrong shapes — reads as "no allowlist" so the
69
+ * bridge degrades to relaying the unfiltered list rather than guessing a
70
+ * narrower one. Call-time enforcement holds either way. */
71
+ export function parseConnectorAllowedToolsEnv(raw) {
72
+ if (!raw)
73
+ return null;
74
+ let value;
75
+ try {
76
+ value = JSON.parse(raw);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ if (!value || typeof value !== "object" || Array.isArray(value))
82
+ return null;
83
+ const grants = {};
84
+ for (const [slug, grant] of Object.entries(value)) {
85
+ if (!CONNECTOR_SLUG_PATTERN.test(slug))
86
+ return null;
87
+ if (!grant || typeof grant !== "object" || Array.isArray(grant))
88
+ return null;
89
+ const tools = grant.tools;
90
+ if (tools === "*") {
91
+ grants[slug] = { tools: "*" };
92
+ continue;
93
+ }
94
+ if (!Array.isArray(tools) || tools.length === 0)
95
+ return null;
96
+ if (!tools.every((tool) => typeof tool === "string" && CONNECTOR_TOOL_NAME_PATTERN.test(tool))) {
97
+ return null;
98
+ }
99
+ grants[slug] = { tools };
100
+ }
101
+ return grants;
102
+ }
103
+ /** Bridge side: decode the connected-service slug list. Anything off —
104
+ * absent, unparseable, or one bad entry — reads as an empty list so the
105
+ * filter degrades to the plain split instead of trusting a partial
106
+ * inventory. */
107
+ export function parseConnectorServiceSlugsEnv(raw) {
108
+ if (!raw)
109
+ return [];
110
+ let value;
111
+ try {
112
+ value = JSON.parse(raw);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ if (!Array.isArray(value))
118
+ return [];
119
+ return value.every((slug) => typeof slug === "string" && CONNECTOR_SLUG_PATTERN.test(slug))
120
+ ? value
121
+ : [];
122
+ }
123
+ /** Whether one advertised tool name survives the filter. A tool is
124
+ * offered when its service is granted with `"*`", when its exact name is
125
+ * granted, or when it is that service's connection flow — a card the
126
+ * person approves, which slice 2 passes through at call time and which a
127
+ * bot granted a service should still be able to offer for another
128
+ * account. */
129
+ export function connectorToolAdvertised(name, grants, candidates = []) {
130
+ if (Object.keys(grants).length > 0 && CONNECTOR_META_TOOLS.includes(name))
131
+ return true;
132
+ // The connected-service slugs are the resolver's candidates, so an
133
+ // underscored service (bland_ai) claims its own tools instead of a
134
+ // plain-prefix grant (bland) widening them; an unreachable catalog
135
+ // leaves the list empty and the plain split stands.
136
+ const service = serviceSlugForCandidates(name, candidates) ?? serviceSlugFor(name);
137
+ const grant = service === null ? undefined : grants[service];
138
+ if (!grant)
139
+ return false;
140
+ if (grant.tools === "*")
141
+ return true;
142
+ if (grant.tools.includes(name))
143
+ return true;
144
+ return CONNECTION_TOOL_SUFFIXES.some((suffix) => name.endsWith(suffix));
145
+ }
146
+ /** Filter one upstream tools/list response — the bridge normalizes SSE and
147
+ * JSON upstreams to a single frame first, so both shapes flow through
148
+ * here. Returns a shallow copy with result.tools trimmed; a frame without
149
+ * a readable tools array passes through unchanged (degrade, never guess).
150
+ * Entries the filter cannot read a name from are kept as-is. */
151
+ export function filterToolsListFrame(frame, grants, candidates = []) {
152
+ const result = frame.result;
153
+ if (!result || typeof result !== "object" || Array.isArray(result))
154
+ return frame;
155
+ const tools = result.tools;
156
+ if (!Array.isArray(tools))
157
+ return frame;
158
+ const kept = tools.filter((tool) => {
159
+ const name = tool && typeof tool === "object" && !Array.isArray(tool)
160
+ ? tool.name
161
+ : undefined;
162
+ return typeof name === "string" ? connectorToolAdvertised(name, grants, candidates) : true;
163
+ });
164
+ return { ...frame, result: { ...result, tools: kept } };
165
+ }
@@ -4,15 +4,24 @@
4
4
  // to the configured Composio Session, but connection requests are converted
5
5
  // into first-class OpenMausBot chat cards. The agent never authors an auth
6
6
  // URL and credentials never pass through its transcript.
7
+ // tools/list responses are trimmed to this bot's granted tools as a
8
+ // prompt-time hint; every call is still judged harness-side by the grants
9
+ // relay.
7
10
  //
8
11
  // stdout is the MCP transport. Never log there.
9
12
  import readline from "node:readline";
10
13
  import { randomUUID } from "node:crypto";
14
+ import { CONNECTOR_ALLOWED_TOOLS_ENV, CONNECTOR_SERVICE_SLUGS_ENV, filterToolsListFrame, parseConnectorAllowedToolsEnv, parseConnectorServiceSlugsEnv, } from "./connector-advertisement.js";
11
15
  const UPSTREAM = process.env.OMB_CONNECTOR_UPSTREAM_URL ?? "";
12
16
  const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
13
17
  const BOT_ID = process.env.OMB_BOT_ID ?? "";
14
18
  const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
15
19
  const TOKEN = process.env.OMB_CONNECTOR_TOKEN ?? process.env.OMB_COMMS_TOKEN ?? "";
20
+ // The bot's effective tool grants, or null when the harness sent none —
21
+ // legacy bots, oversized allowlists, anything unreadable. null means the
22
+ // upstream list is relayed verbatim; the harness still judges every call.
23
+ const ALLOWED_TOOLS = parseConnectorAllowedToolsEnv(process.env[CONNECTOR_ALLOWED_TOOLS_ENV]);
24
+ const SERVICE_SLUGS = parseConnectorServiceSlugsEnv(process.env[CONNECTOR_SERVICE_SLUGS_ENV]);
16
25
  const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
17
26
  const INITIALIZE_RELAY_TIMEOUT_MS = 1_000;
18
27
  const RELAY_TIMEOUT_MS = 10 * 60_000;
@@ -210,8 +219,11 @@ async function handle(message) {
210
219
  }
211
220
  try {
212
221
  const response = await relay(message);
213
- if (response && id !== undefined)
214
- send(response);
222
+ if (response && id !== undefined) {
223
+ send(method === "tools/list" && ALLOWED_TOOLS
224
+ ? filterToolsListFrame(response, ALLOWED_TOOLS, SERVICE_SLUGS)
225
+ : response);
226
+ }
215
227
  }
216
228
  catch (error) {
217
229
  if (id === undefined)
@@ -0,0 +1,168 @@
1
+ // The harness-side verdict for connected-app tool calls (issue #1736, part
2
+ // of the per-bot Composio tool grants umbrella #1734).
3
+ //
4
+ // Every Composio tool call crosses POST /api/internal/connectors/mcp: the
5
+ // provider CLIs talk to the stdio bridge, and the bridge forwards each
6
+ // JSON-RPC frame there. So the grants verdict lives on the harness side,
7
+ // where the loopback token already names the bot and the decision log is
8
+ // native — the bridge never sees a key it could have to enforce with.
9
+ //
10
+ // Three shapes reach this module:
11
+ //
12
+ // * a direct per-toolkit call — params.name is the tool itself, e.g.
13
+ // GMAIL_SEND_EMAIL;
14
+ // * the executor meta-tool — COMPOSIO_MULTI_EXECUTE_TOOL, whose
15
+ // arguments name one or many target tools;
16
+ // * everything else — connection cards, discovery, notifications —
17
+ // which passes through untouched (advertisement filtering is slice 3).
18
+ //
19
+ // Unrecognized argument shapes are denied by default: a call whose target
20
+ // tools cannot be read is a call whose tools cannot be checked, and the
21
+ // grants promise — "this bot may call exactly these tools" — must hold
22
+ // even against a model that garbles the protocol.
23
+ import { CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
24
+ /** The executor meta-tool: the only meta-tool whose arguments name target
25
+ * tools. Discovery and connection meta-tools keep their existing flows. */
26
+ export const COMPOSIO_MULTI_EXECUTE_TOOL = "COMPOSIO_MULTI_EXECUTE_TOOL";
27
+ /** The service a Composio tool name belongs to: the upper-snake prefix
28
+ * before the first underscore, lowercased (GMAIL_SEND_EMAIL to gmail). A
29
+ * name with no underscore names no service, so no grant can cover it. */
30
+ export function serviceSlugFor(tool) {
31
+ const underscore = tool.indexOf("_");
32
+ if (underscore <= 0)
33
+ return null;
34
+ return tool.slice(0, underscore).toLowerCase();
35
+ }
36
+ /** The service a Composio tool name belongs to when the caller knows the
37
+ * real service slugs. Slugs may contain underscores (bland_ai), which the
38
+ * plain first-segment split above cannot see — BLAND_AI_MAKE_CALL splits
39
+ * as "bland". When a known candidate matches the name's prefix, the
40
+ * longest candidate wins (bland_ai over bland), so an underscored service
41
+ * keeps its own tools; with no matching candidate the caller falls back
42
+ * to serviceSlugFor. */
43
+ export function serviceSlugForCandidates(tool, candidates) {
44
+ let match = null;
45
+ for (const candidate of candidates) {
46
+ if (tool.startsWith(candidate.toUpperCase() + "_") && (match === null || candidate.length > match.length)) {
47
+ match = candidate;
48
+ }
49
+ }
50
+ return match;
51
+ }
52
+ /** Classify one relayed JSON-RPC frame. Anything that is not a tools/call,
53
+ * or is a discovery/connection/platform meta-tool, passes through. */
54
+ export function connectorCallFromFrame(payload) {
55
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
56
+ return { kind: "passthrough" };
57
+ const frame = payload;
58
+ if (frame.method !== "tools/call")
59
+ return { kind: "passthrough" };
60
+ const params = frame.params;
61
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
62
+ return { kind: "unrecognized", invoked: "tools/call", reason: "the call carried no params object" };
63
+ }
64
+ const name = params.name;
65
+ if (typeof name !== "string" || !name) {
66
+ return { kind: "unrecognized", invoked: "tools/call", reason: "the call named no tool" };
67
+ }
68
+ if (name === COMPOSIO_MULTI_EXECUTE_TOOL) {
69
+ return multiExecuteCall(name, params.arguments);
70
+ }
71
+ // Platform meta-tools (search, schemas, remote workbench) are not
72
+ // connected-app tools; grants are keyed by service slug, and slice 3
73
+ // owns what the model is even shown.
74
+ if (name.startsWith("COMPOSIO_"))
75
+ return { kind: "passthrough" };
76
+ // Per-service connection cards keep their card flow in any spelling.
77
+ if (name.endsWith("_MANAGE_CONNECTIONS") || name.endsWith("_WAIT_FOR_CONNECTIONS")) {
78
+ return { kind: "passthrough" };
79
+ }
80
+ if (!CONNECTOR_TOOL_NAME_PATTERN.test(name)) {
81
+ return { kind: "unrecognized", invoked: name, reason: "the tool name is not a Composio tool name" };
82
+ }
83
+ return { kind: "tools", invoked: name, names: [name] };
84
+ }
85
+ /** Read the target tools out of COMPOSIO_MULTI_EXECUTE_TOOL arguments:
86
+ * the upstream schema is a 1-50 item tools array of { tool_slug,
87
+ * arguments }; a legacy single { tool_slug } object is accepted too.
88
+ * Anything else cannot be checked, so it is denied. */
89
+ function multiExecuteCall(invoked, args) {
90
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
91
+ return { kind: "unrecognized", invoked, reason: "the arguments were not an object" };
92
+ }
93
+ const tools = args.tools;
94
+ const singleSlug = args.tool_slug;
95
+ if (Array.isArray(tools)) {
96
+ if (tools.length === 0) {
97
+ return { kind: "unrecognized", invoked, reason: "the tools list was empty" };
98
+ }
99
+ const names = [];
100
+ for (const item of tools) {
101
+ const slug = item && typeof item === "object" && !Array.isArray(item)
102
+ ? item.tool_slug
103
+ : undefined;
104
+ if (typeof slug !== "string" || !slug) {
105
+ return { kind: "unrecognized", invoked, reason: "an entry in the tools list named no tool_slug" };
106
+ }
107
+ names.push(slug);
108
+ }
109
+ return { kind: "tools", invoked, names };
110
+ }
111
+ if (typeof singleSlug === "string" && singleSlug) {
112
+ return { kind: "tools", invoked, names: [singleSlug] };
113
+ }
114
+ return { kind: "unrecognized", invoked, reason: "the arguments named no tools to execute" };
115
+ }
116
+ /** Judge distinct target names against a bot's grants. grants undefined
117
+ * is the legacy all-tools bot and passes everything; an explicit record —
118
+ * including the empty one — allows only what it names. Service keys are
119
+ * read as own properties only: a name like CONSTRUCTOR_X maps to the
120
+ * "constructor" service, and the inherited property must never pose as
121
+ * a grant.
122
+ * candidates are the caller's connected-service slugs: the real backend
123
+ * slugs, so an underscored service (bland_ai) keeps its own tools instead
124
+ * of a plain-prefix grant (bland) capturing them. An empty list means the
125
+ * connected-service catalog was unreachable, and the plain first-segment
126
+ * split stands as the fallback — call-time enforcement degrades open, the
127
+ * same way advertisement filtering does. */
128
+ export function evaluateConnectorTools(names, grants, candidates = []) {
129
+ if (grants === undefined) {
130
+ return { allowed: true, legacy: true, denials: [], rule: "composio" };
131
+ }
132
+ const denials = [];
133
+ let rule = "";
134
+ for (const tool of new Set(names)) {
135
+ const service = serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool);
136
+ const grant = service !== null && Object.hasOwn(grants, service)
137
+ ? grants[service]
138
+ : undefined;
139
+ if (grant && (grant.tools === "*" || grant.tools.includes(tool))) {
140
+ if (!rule)
141
+ rule = "connectorTools." + service;
142
+ continue;
143
+ }
144
+ denials.push({ tool, service, onGrantedService: Boolean(grant) });
145
+ }
146
+ return { allowed: denials.length === 0, legacy: false, denials, rule };
147
+ }
148
+ /** Safe to hand straight to the model: names the refused tool and says who
149
+ * can change it. It never lists what the bot could have called instead —
150
+ * a refusal that enumerates adjacent grants teaches the model to probe. */
151
+ export function connectorRefusalText(denials) {
152
+ const named = denials.map((denial) => '"' + denial.tool + '"').join(", ");
153
+ const one = denials.length === 1;
154
+ return [
155
+ named + " " + (one ? "is" : "are") + " not granted to this bot.",
156
+ "This call was not performed.",
157
+ "Ask the person to grant " + (one ? "it" : "these tools") + " in OpenMausBot if they want " + (one ? "it" : "them") + " run.",
158
+ ].join(" ");
159
+ }
160
+ /** The unrecognized-shape refusal: names the meta-tool and what could not
161
+ * be read, and still points at the person rather than at any tool list. */
162
+ export function connectorUnrecognizedText(invoked, reason) {
163
+ return [
164
+ "A " + invoked + " call arrived in a shape OpenMausBot could not read (" + reason + "), so it was not performed.",
165
+ "Send a well-formed call that names the tools to execute.",
166
+ "Ask the person to grant any tool this bot needs.",
167
+ ].join(" ");
168
+ }
@@ -310,7 +310,7 @@ function statusProblem(status) {
310
310
  if (status.security === "unsafe")
311
311
  return "The existing Local VM is missing safety limits; recreate it";
312
312
  if (status.persistence === "unsafe")
313
- return "The existing Local VM is missing its durable workspace; recreate it";
313
+ return "The existing Local VM is missing its durable folder; recreate it";
314
314
  if (status.container === "stopped")
315
315
  return "This desktop image cannot safely resume; recreate the Local VM";
316
316
  if (status.desktop_error)
@@ -808,6 +808,66 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
808
808
  }
809
809
  const screenshotStatusCache = new Map();
810
810
  const containerMcpPath = SPAWNED_PROXIES.containerMcp;
811
+ const EXEC_DEFAULT_SECONDS = 60;
812
+ const EXEC_MAX_SECONDS = 300;
813
+ const EXEC_OUTPUT_LIMIT = 20_000;
814
+ /** Keep the start and the end: a failure message is nearly always at the end. */
815
+ export function clipExecOutput(text, limit = EXEC_OUTPUT_LIMIT) {
816
+ if (text.length <= limit)
817
+ return text;
818
+ const head = Math.floor(limit / 5);
819
+ return `${text.slice(0, head)}\n… ${text.length - limit} characters omitted …\n${text.slice(text.length - (limit - head))}`;
820
+ }
821
+ const defaultExecRunner = async (command, args, options) => {
822
+ const resolved = resolveCliSpawn(command, args);
823
+ try {
824
+ const { stdout, stderr } = await run(resolved.command, resolved.args, {
825
+ timeout: options.timeout,
826
+ encoding: "utf8",
827
+ maxBuffer: 64 * 1024 * 1024,
828
+ env: { ...process.env, PATH: augmentedPath() },
829
+ });
830
+ return { stdout, stderr, code: 0 };
831
+ }
832
+ catch (error) {
833
+ const failure = error;
834
+ // A non-zero exit still carries the output; anything else (runtime missing,
835
+ // client killed) is a real failure to run at all.
836
+ if (typeof failure.code === "number") {
837
+ return { stdout: failure.stdout ?? "", stderr: failure.stderr ?? "", code: failure.code };
838
+ }
839
+ throw Object.assign(new Error(failure.killed ? "the command client timed out" : failure.message ?? "could not run the command"), { status: 502 });
840
+ }
841
+ };
842
+ /** Run one shell command inside a Local VM as the desktop user, in its durable
843
+ * workspace, and return the exit code and text output. Bots use this instead of
844
+ * typing into a terminal window and reading screenshots. The limit is enforced
845
+ * inside the container so a runaway process is really stopped. */
846
+ export async function containerExec(target, command, options = {}) {
847
+ if (!command.trim())
848
+ throw Object.assign(new Error("command is required"), { status: 400 });
849
+ if (command.length > 20_000)
850
+ throw Object.assign(new Error("command is too long"), { status: 400 });
851
+ if (options.timeoutSeconds !== undefined && !Number.isFinite(options.timeoutSeconds)) {
852
+ throw Object.assign(new Error("timeout_seconds must be finite"), { status: 400 });
853
+ }
854
+ const runtime = options.runtime ?? (await containerRuntimeStatus()).runtime;
855
+ if (!runtime)
856
+ throw Object.assign(new Error("No container runtime is available for the Local VM"), { status: 409 });
857
+ const seconds = Math.min(Math.max(Math.floor(options.timeoutSeconds ?? EXEC_DEFAULT_SECONDS), 1), EXEC_MAX_SECONDS);
858
+ const result = await (options.exec ?? defaultExecRunner)(runtime, [
859
+ "exec",
860
+ "-u", "cua",
861
+ "-w", VM_WORKSPACE_GUEST,
862
+ "-e", "HOME=/home/cua",
863
+ "-e", `DISPLAY=${DISPLAY}`,
864
+ target.containerName,
865
+ "timeout", "-k", "5", String(seconds),
866
+ "sh", "-lc", command,
867
+ ], { timeout: (seconds + 20) * 1000 });
868
+ const timedOut = result.code === 124 || result.code === 137;
869
+ return { exitCode: result.code, stdout: clipExecOutput(result.stdout), stderr: clipExecOutput(result.stderr), timedOut };
870
+ }
811
871
  export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
812
872
  return {
813
873
  command: process.execPath,
@@ -17,6 +17,28 @@ export function isModelVariant(value) {
17
17
  return typeof value === "string" && value.length > 0 && value.length <= 256 &&
18
18
  value.trim() === value && !/\p{Cc}/u.test(value);
19
19
  }
20
+ /** The picker label for a model id when the catalog row carries no display
21
+ * name: split on the word breaks the catalog treats as separators and
22
+ * capitalize each part ("gpt-5.4-mini" → "Gpt 5.4 Mini"). Each harness
23
+ * passes its own break class so existing labels stay byte-for-byte. */
24
+ export function titleCaseModelId(id, wordBreaks) {
25
+ return id
26
+ .split(wordBreaks)
27
+ .filter(Boolean)
28
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29
+ .join(" ");
30
+ }
31
+ /** A picker label that carries a second facet — a display name, a provider
32
+ * host — beside the model id, unless the facet is already what the id says.
33
+ * `redundantWhen` decides what counts as "already said" and `decorate` how
34
+ * the facet is appended, so every harness's labels stay exactly what they
35
+ * were while the shape is written once. */
36
+ export function qualifiedModelLabel(id, qualifier, options = {}) {
37
+ const { redundantWhen = () => false, decorate = (facet) => ` — ${facet}` } = options;
38
+ if (!qualifier || redundantWhen(id, qualifier))
39
+ return id;
40
+ return `${id}${decorate(qualifier)}`;
41
+ }
20
42
  let eventCounter = 0;
21
43
  export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
22
44
  export const newId = () => crypto.randomUUID();
@@ -37,3 +37,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
37
37
  }
38
38
  return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
39
39
  }
40
+ /** Complete a new bot's selection with the workspace's new-bot effort. An
41
+ * explicit effort or model variant is the caller's choice and wins; an engine
42
+ * that does not offer the level keeps sending none rather than failing turn 1. */
43
+ export function withNewBotEffort(selection, effort, offered) {
44
+ if (!effort || selection.effort !== undefined || selection.variant !== undefined || !offered?.includes(effort)) {
45
+ return selection;
46
+ }
47
+ return { ...selection, effort };
48
+ }