openmausbot 0.1.85 → 0.1.87

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
  62. package/dist/assets/arc-C88Blc2v.js +1 -0
  63. package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
  64. package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
  65. package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
  66. package/dist/assets/channel-taCNRlUU.js +1 -0
  67. package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
  68. package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
  69. package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
  70. package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
  71. package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
  72. package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
  73. package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
  74. package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
  75. package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
  76. package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
  77. package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
  78. package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
  79. package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
  80. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  81. package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
  82. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  83. package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
  84. package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
  85. package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
  86. package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
  87. package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
  88. package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
  89. package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
  90. package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
  91. package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
  92. package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
  93. package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
  94. package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
  95. package/dist/assets/graph-D8UmR1vK.js +1 -0
  96. package/dist/assets/index-BwdMAQ6p.css +1 -0
  97. package/dist/assets/index-ByJennr7.js +581 -0
  98. package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
  99. package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
  102. package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
  103. package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
  104. package/dist/assets/linear-DBJGPmjT.js +1 -0
  105. package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
  106. package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
  107. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  108. package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
  109. package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
  110. package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
  111. package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
  112. package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
  113. package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
  114. package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
  115. package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
  116. package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
  117. package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
  118. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  119. package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
  120. package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
  121. package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
  122. package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
  123. package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
  124. package/dist/index.html +2 -2
  125. package/dist-server/browser-proxy.js +2 -1
  126. package/dist-server/container-mcp.js +1047 -173
  127. package/dist-server/drivers/agents-proxy.js +16121 -1504
  128. package/dist-server/drivers/phone-proxy.js +33 -2
  129. package/dist-server/enterprise/LICENSE +32 -0
  130. package/dist-server/enterprise/server/index.js +5 -5
  131. package/dist-server/index.js +16134 -7446
  132. package/dist-server/local-computer-proxy.js +3 -3
  133. package/dist-server/local-computer.js +53 -17
  134. package/dist-server/mcp-server.js +3 -1
  135. package/dist-server/openmausbot.js +9353 -6124
  136. package/dist-server/pair-cli.js +9353 -6124
  137. package/dist-server/permission-proxy.js +29 -9
  138. package/dist-server/server/admin-activity.js +410 -0
  139. package/dist-server/server/attachments.js +55 -0
  140. package/dist-server/server/auto-approve.js +4 -3
  141. package/dist-server/server/auto-vm-claims.js +1 -0
  142. package/dist-server/server/bot-package.js +18 -292
  143. package/dist-server/server/bot-profile.js +5 -17
  144. package/dist-server/server/bot-visibility.js +462 -0
  145. package/dist-server/server/box-create-idempotency.js +3 -3
  146. package/dist-server/server/box.js +59 -56
  147. package/dist-server/server/browser-proxy.js +3 -2
  148. package/dist-server/server/browser-runtime.js +38 -2
  149. package/dist-server/server/channel-queue.js +2 -0
  150. package/dist-server/server/cli-prompts.js +24 -2
  151. package/dist-server/server/cli.js +81 -12
  152. package/dist-server/server/command-allowlist.js +154 -0
  153. package/dist-server/server/composio.js +49 -11
  154. package/dist-server/server/computer-wait.js +22 -9
  155. package/dist-server/server/config.js +133 -17
  156. package/dist-server/server/connector-verdict.js +141 -0
  157. package/dist-server/server/container-computer.js +1 -1
  158. package/dist-server/server/decision-log.js +209 -48
  159. package/dist-server/server/default-model-selection.js +29 -3
  160. package/dist-server/server/delegations.js +97 -44
  161. package/dist-server/server/drivers/acp/core.js +163 -33
  162. package/dist-server/server/drivers/acp/grok.js +4 -3
  163. package/dist-server/server/drivers/agents-call.js +1162 -0
  164. package/dist-server/server/drivers/agents-catalog.js +689 -0
  165. package/dist-server/server/drivers/agents-client.js +28 -0
  166. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  167. package/dist-server/server/drivers/boxagent.js +174 -36
  168. package/dist-server/server/drivers/builtIn.js +2 -0
  169. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  170. package/dist-server/server/drivers/chat-images.js +54 -0
  171. package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
  172. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  173. package/dist-server/server/drivers/claude.js +28 -8
  174. package/dist-server/server/drivers/codex-instructions.js +48 -6
  175. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  176. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  177. package/dist-server/server/drivers/codex.js +121 -34
  178. package/dist-server/server/drivers/grok.js +2 -1
  179. package/dist-server/server/drivers/mistral.js +115 -0
  180. package/dist-server/server/drivers/openai-chat.js +155 -32
  181. package/dist-server/server/drivers/openai-compat.js +16 -8
  182. package/dist-server/server/drivers/permission-command.js +35 -0
  183. package/dist-server/server/drivers/phone-proxy.js +47 -2
  184. package/dist-server/server/drivers/pi.js +75 -4
  185. package/dist-server/server/drivers/prompt-split.js +87 -0
  186. package/dist-server/server/enterprise.js +97 -16
  187. package/dist-server/server/external-runtime.js +83 -0
  188. package/dist-server/server/fleet.js +1 -1
  189. package/dist-server/server/group-thread-usage.js +103 -0
  190. package/dist-server/server/guarded-requests.js +60 -0
  191. package/dist-server/server/harness/http.js +61 -0
  192. package/dist-server/server/harness/registry.js +1 -0
  193. package/dist-server/server/hosted-models.js +151 -0
  194. package/dist-server/server/hosted-slack.js +11 -0
  195. package/dist-server/server/index.js +3091 -658
  196. package/dist-server/server/local-computer.js +72 -23
  197. package/dist-server/server/managed-desktop.js +79 -14
  198. package/dist-server/server/managed-policy.js +153 -0
  199. package/dist-server/server/mcp-bridge.js +6 -6
  200. package/dist-server/server/message-db.js +28 -0
  201. package/dist-server/server/model-context-window.js +7 -0
  202. package/dist-server/server/model-prices.js +121 -0
  203. package/dist-server/server/new-bot-defaults.js +115 -0
  204. package/dist-server/server/notify.js +10 -1
  205. package/dist-server/server/org-library.js +1051 -0
  206. package/dist-server/server/package-export.js +627 -39
  207. package/dist-server/server/package-import.js +458 -0
  208. package/dist-server/server/package-parts.js +87 -0
  209. package/dist-server/server/peer-approval.js +16 -7
  210. package/dist-server/server/peer-roster.js +23 -2
  211. package/dist-server/server/presets.js +492 -0
  212. package/dist-server/server/provider-key-check.js +3 -2
  213. package/dist-server/server/published-teams.js +80 -0
  214. package/dist-server/server/recent-work.js +15 -0
  215. package/dist-server/server/request-auth.js +96 -0
  216. package/dist-server/server/room-handoffs.js +44 -14
  217. package/dist-server/server/routes/bot-presets.js +25 -0
  218. package/dist-server/server/routes/hosted-slack.js +20 -0
  219. package/dist-server/server/routes/table.js +14 -0
  220. package/dist-server/server/routine-requests.js +6 -3
  221. package/dist-server/server/routines.js +63 -1
  222. package/dist-server/server/section-context.js +10 -0
  223. package/dist-server/server/skills.js +34 -61
  224. package/dist-server/server/spend.js +111 -16
  225. package/dist-server/server/steer-queue.js +9 -3
  226. package/dist-server/server/store.js +550 -35
  227. package/dist-server/server/surface.js +10 -4
  228. package/dist-server/server/system-prompt.js +12 -4
  229. package/dist-server/server/team-backup.js +16 -3
  230. package/dist-server/server/team-computers.js +10 -0
  231. package/dist-server/server/team-library.js +6 -2
  232. package/dist-server/server/team-manifest.js +4 -3
  233. package/dist-server/server/team-setup-requests.js +16 -5
  234. package/dist-server/server/thread-events.js +17 -1
  235. package/dist-server/server/thread-starters.js +53 -0
  236. package/dist-server/server/turn-context.js +2 -0
  237. package/dist-server/server/turn-watchdog.js +12 -3
  238. package/dist-server/server/usage-ledger.js +43 -9
  239. package/dist-server/server/vps-computer.js +139 -51
  240. package/dist-server/server/vps-container-mcp.js +7 -0
  241. package/dist-server/server/vps-ssh.js +36 -7
  242. package/dist-server/server/workspace-backup-policy.js +21 -2
  243. package/dist-server/server/workspace-backup.js +7 -4
  244. package/dist-server/server/workspace.js +1 -1
  245. package/dist-server/shared/approval-mode.js +1 -1
  246. package/dist-server/shared/ask-question.js +188 -12
  247. package/dist-server/shared/bot-profile.js +15 -0
  248. package/dist-server/shared/command-allowlist.js +1 -0
  249. package/dist-server/shared/credential-request.js +1 -1
  250. package/dist-server/shared/mention-boundary.js +15 -0
  251. package/dist-server/shared/package-format.js +967 -0
  252. package/dist-server/shared/routines.js +7 -1
  253. package/dist-server/shared/skill-md.js +64 -0
  254. package/dist-server/shared/team-backup.js +9 -0
  255. package/dist-server/shared/wire.js +4 -0
  256. package/dist-server/src/lib/composer-attachments.js +16 -0
  257. package/dist-server/vps-container-mcp.js +1147 -179
  258. package/enterprise/README.md +33 -12
  259. package/enterprise/server/index.js +5 -5
  260. package/package.json +1 -1
  261. package/dist/assets/index-Ba9G44HI.js +0 -310
  262. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -0,0 +1,458 @@
1
+ // Adding a shared team to this installation.
2
+ //
3
+ // Import is additive-only. A package is untrusted input (a file someone
4
+ // sent, GitHub, the community catalog), so it is structurally unable to reach
5
+ // records the person already has: every bot, group chat, routine, section and
6
+ // connection it creates is NEW, and every privilege-bearing field is chosen
7
+ // here, never read from the file. What the file may set is persona and
8
+ // structure: names, standing instructions, looks, playbooks, skills (which
9
+ // land switched off), group chats, routines (which land paused), the team
10
+ // brief and Chief, starter notes, and connection SLOTS (an address and the
11
+ // names of values the person fills in later; never the values).
12
+ //
13
+ // `trust` is a function argument, never read from a request. The HTTP import
14
+ // route always passes "file". Only the organization library (wave 2, a
15
+ // separate module) passes "org": its skills arrive switched on because the
16
+ // organization's Admin published them, and its records carry the provenance
17
+ // the organization channel needs. Everything else is identical.
18
+ //
19
+ // A failure anywhere rolls back every record this import created.
20
+ import { createHash, randomUUID } from "node:crypto";
21
+ import { mcpServerNameError, MAX_MCP_SERVERS, parseStoredMcpServer } from "./mcp-registry.js";
22
+ import { AGENT_PARTS, agentReleaseValues, connectionLocalValue, connectionReleaseValue, pair, pairs, playbookValues, ROOM_PARTS, roomReleaseValues, ROUTINE_PARTS, routineReleaseValues, scheduleValue, sha256Hex, TEAM_PARTS, } from "./package-parts.js";
23
+ import { importedMemberProfile } from "./team-manifest.js";
24
+ import { takeImportName } from "../shared/import-name.js";
25
+ import { decodeBase64 } from "../shared/package-format.js";
26
+ export const NO_BOTS_MESSAGE = "This package has no bots. Add it from your organization's shelf, or update OpenMausBot.";
27
+ /** A file with skills only: a file never adds a skill without a bot or a preset to hold it. */
28
+ export const NO_PRESETS_MESSAGE = "This file has no bots or preset bots to add. Its skills can be added from your organization's shelf.";
29
+ /** A refusal before anything was created, as a plain sentence. */
30
+ export class PackageImportError extends Error {
31
+ status = 400;
32
+ code;
33
+ constructor(code, message) {
34
+ super(message);
35
+ this.name = "PackageImportError";
36
+ this.code = code;
37
+ }
38
+ }
39
+ /** Deterministic, so reconnecting to the same organization recognizes what
40
+ * it already added. `packageId` is Admin's package UUID. */
41
+ export function orgInstallId(adminOrigin, organizationId, packageId) {
42
+ return createHash("sha256").update(`omb-install:v1\n${adminOrigin}\n${organizationId}\n${packageId}`, "utf8").digest("hex").slice(0, 32);
43
+ }
44
+ function memberFromAgent(agent) {
45
+ return {
46
+ key: agent.key,
47
+ name: agent.name,
48
+ title: agent.title ?? "",
49
+ description: agent.description ?? "",
50
+ ...(agent.soul !== undefined ? { soul: agent.soul } : {}),
51
+ appearance: {
52
+ color: agent.appearance.color,
53
+ ...(agent.appearance.mascotExpression ? { mascotExpression: agent.appearance.mascotExpression } : {}),
54
+ ...(agent.appearance.mascotBody ? { mascotBody: agent.appearance.mascotBody } : {}),
55
+ },
56
+ };
57
+ }
58
+ /** The first free server name for a slot: key, key-2, … (never an existing
59
+ * or reserved name, so a slot can never bind to credentials already here). */
60
+ function freeServerName(key, taken) {
61
+ for (let n = 1; n < 100; n += 1) {
62
+ const name = n === 1 ? key : `${key}-${n}`;
63
+ if (name.length > 32)
64
+ return null;
65
+ if (mcpServerNameError(name) || Object.hasOwn(taken, name))
66
+ continue;
67
+ return name;
68
+ }
69
+ return null;
70
+ }
71
+ /** Add a shared team (v1 or v2, already parsed) as new records. */
72
+ export function importPackageDocument(document, options, deps) {
73
+ const pkg = document.package;
74
+ const library = !pkg.agents.length || !pkg.team;
75
+ // A library package (skills and presets, no bots) from a file needs the preset store.
76
+ if (library && options.trust !== "org" && !deps.presets)
77
+ throw new PackageImportError("no_bots", NO_BOTS_MESSAGE);
78
+ if (options.trust === "org") {
79
+ const org = options.org;
80
+ if (!org)
81
+ throw new PackageImportError("invalid_package", "This package is missing its organization details.");
82
+ if (org.installId !== orgInstallId(org.adminOrigin, org.organizationId, org.packageId)) {
83
+ throw new PackageImportError("invalid_package", "This package's install id does not match its organization.");
84
+ }
85
+ if (pkg.publisher?.organization !== org.publisher.slug) {
86
+ throw new PackageImportError("invalid_package", "This package was not published by the organization that shared it.");
87
+ }
88
+ if (org.ref !== `${org.publisher.slug}/${pkg.id}`) {
89
+ throw new PackageImportError("invalid_package", "This package is not the one your organization listed.");
90
+ }
91
+ if (!deps.skills.installOrg || !deps.routines.stampInstalledPackage) {
92
+ throw new PackageImportError("invalid_package", "This installation cannot add packages from an organization.");
93
+ }
94
+ // A bot someone made from one of the install's presets carries its
95
+ // install id too, but it is theirs, not one of the package's records;
96
+ // preset rows are not records either. Re-adding a removed team, or
97
+ // retrying after a crash, re-registers its presets in place (presets.ts).
98
+ if (deps.store.bots.some((bot) => bot.installedPackage?.installId === org.installId && !bot.installedPackage.presetKey) ||
99
+ deps.store.groups.some((group) => group.installedPackage?.installId === org.installId)) {
100
+ return { alreadyAdded: true, installId: org.installId };
101
+ }
102
+ // A library package (skills and presets, no team) creates no bots:
103
+ // its skills are offered under Bot → Skills, its presets go to New bot,
104
+ // and the organization library keeps the index of what it offers.
105
+ if (library) {
106
+ const registered = deps.presets?.register(document, { source: "org", installId: org.installId, org });
107
+ return {
108
+ alreadyAdded: false, installId: org.installId, name: pkg.name, section: "", bots: [], groups: [], routines: [],
109
+ offeredSkills: (pkg.skills?.entries ?? []).map((skill) => skill.name), connections: [],
110
+ skipped: registered?.skipped ?? (pkg.presets ?? []).map((preset) => ({ part: `presets[${preset.key}]`, reason: "presets_not_supported_yet" })),
111
+ brief: false, notes: 0,
112
+ ...(registered ? { presets: [...registered.added, ...registered.existing] } : {}),
113
+ org: { kind: "library", section: "", team: { parts: {} }, bots: {}, rooms: {}, routines: {}, connections: {} },
114
+ };
115
+ }
116
+ }
117
+ if (library)
118
+ return importLibraryFile(document, deps.presets);
119
+ return runImport({ kind: "package", document }, options, deps);
120
+ }
121
+ /** A preset file (skills and presets, no team): the presets go to New bot,
122
+ * and the skills are offered (a file never installs a skill without a bot or
123
+ * a preset to hold it). */
124
+ function importLibraryFile(document, presets) {
125
+ const pkg = document.package;
126
+ if (!pkg.presets?.length)
127
+ throw new PackageImportError("no_bots", NO_PRESETS_MESSAGE);
128
+ const installId = randomUUID();
129
+ const registered = presets.register(document, { source: "file", installId });
130
+ return {
131
+ alreadyAdded: false, installId, name: pkg.name, section: "", bots: [], groups: [], routines: [],
132
+ offeredSkills: (pkg.skills?.entries ?? []).map((skill) => skill.name), connections: [],
133
+ skipped: registered.skipped, brief: false, notes: 0, presets: [...registered.added, ...registered.existing],
134
+ };
135
+ }
136
+ /** Add a legacy `openmaus.team` file: people only (plus a project room when
137
+ * the caller asks for one). */
138
+ export function importTeamManifest(manifest, options, deps) {
139
+ return runImport({ kind: "manifest", manifest }, { ...options, trust: "file" }, deps);
140
+ }
141
+ function runImport(source, options, deps) {
142
+ const { store } = deps;
143
+ const pkg = source.kind === "package" ? source.document.package : null;
144
+ const name = pkg?.name ?? (source.kind === "manifest" ? source.manifest.team.name : "");
145
+ const teamName = pkg?.team?.name ?? name;
146
+ const org = options.trust === "org" ? options.org : undefined;
147
+ const installId = pkg ? org?.installId ?? randomUUID() : undefined;
148
+ const members = pkg
149
+ ? pkg.agents.map((agent) => ({ member: memberFromAgent(agent), agent }))
150
+ : source.manifest.team.members.map((member) => ({ member, agent: undefined }));
151
+ const importedBots = [];
152
+ const createdGroups = [];
153
+ const createdRoutines = [];
154
+ const createdServers = [];
155
+ const createdPresets = [];
156
+ const skipped = [];
157
+ const orgIndex = { kind: "team", section: "", team: { parts: {} }, bots: {}, rooms: {}, routines: {}, connections: {} };
158
+ // Names already in use, hidden bots included: an archived bot can be
159
+ // un-archived later, and a revived duplicate would be just as ambiguous.
160
+ const takenNames = new Set(store.bots.map((bot) => bot.name.trim().toLowerCase()));
161
+ const botIds = new Map();
162
+ let group;
163
+ let section;
164
+ let notes = 0;
165
+ try {
166
+ const selection = deps.defaultSelection();
167
+ const existingSections = new Set([...store.sections, ...store.bots.map((bot) => bot.section), ...store.groups.map((candidate) => candidate.section)]
168
+ .filter((value) => Boolean(value?.trim()))
169
+ .map((value) => value.trim().toLowerCase()));
170
+ // Every template gets its own new section, numbered on collision. Never
171
+ // merge into an existing section (or replace its Chief).
172
+ section = takeImportName(teamName, existingSections, 60);
173
+ // Connection slots first, so bots can be bound to exactly these servers.
174
+ const serverForKey = new Map();
175
+ const connections = [];
176
+ if (pkg?.connections?.length) {
177
+ const next = { ...deps.mcp.servers() };
178
+ for (const connection of pkg.connections) {
179
+ const part = `connections[${connection.key}]`;
180
+ if (Object.keys(next).length >= MAX_MCP_SERVERS) {
181
+ skipped.push({ part, reason: "too_many_connections" });
182
+ continue;
183
+ }
184
+ const serverName = freeServerName(connection.key, next);
185
+ const server = {
186
+ type: connection.mcp.transport,
187
+ url: connection.mcp.url,
188
+ ...(connection.mcp.valueNames.length ? { headers: Object.fromEntries(connection.mcp.valueNames.map((header) => [header, ""])) } : {}),
189
+ // Off until the person fills in the values and switches it on.
190
+ enabled: false,
191
+ };
192
+ if (!serverName || !parseStoredMcpServer(serverName, server).ok) {
193
+ skipped.push({ part, reason: "connection_invalid" });
194
+ continue;
195
+ }
196
+ if (deps.mcp.refusal(serverName, server)) {
197
+ skipped.push({ part, reason: "connection_refused_by_policy" });
198
+ continue;
199
+ }
200
+ next[serverName] = server;
201
+ createdServers.push(serverName);
202
+ serverForKey.set(connection.key, serverName);
203
+ connections.push({ key: connection.key, name: serverName, label: connection.label });
204
+ if (org) {
205
+ orgIndex.connections[connection.key] = { name: serverName, ...pair(connectionReleaseValue(connection), connectionLocalValue(server)) };
206
+ }
207
+ }
208
+ if (createdServers.length)
209
+ deps.mcp.persist(next);
210
+ }
211
+ const playbookByKey = new Map((pkg?.playbooks ?? []).map((playbook) => [playbook.key, playbook]));
212
+ const skillByName = new Map((pkg?.skills?.entries ?? []).map((skill) => [skill.name, skill]));
213
+ for (const { member, agent } of members) {
214
+ // importedMemberProfile is the authority boundary: persona fields
215
+ // only, colliding names numbered. seedMessages: false — an imported
216
+ // bot must not open by greeting the user as though it were new.
217
+ const created = store.createBot({
218
+ ...importedMemberProfile(member, takenNames),
219
+ modelSelection: selection,
220
+ section,
221
+ ...(options.visibility ? { visibility: options.visibility } : {}),
222
+ }, { seedMessages: false });
223
+ importedBots.push(created);
224
+ botIds.set(member.key, created.id);
225
+ const playbooks = (agent?.playbooks ?? []).flatMap((key) => {
226
+ const playbook = playbookByKey.get(key);
227
+ return playbook ? [{ ...playbook }] : [];
228
+ });
229
+ let avatar = {};
230
+ let avatarHash = null;
231
+ if (agent?.appearance.avatar) {
232
+ const bytes = decodeBase64(agent.appearance.avatar.data);
233
+ if (!bytes)
234
+ throw new Error(`The picture for ${member.name} could not be read`);
235
+ avatar = { avatarUrl: deps.images.save(bytes, agent.appearance.avatar.mime), avatarCrop: agent.appearance.avatar.crop };
236
+ avatarHash = sha256Hex(bytes);
237
+ }
238
+ const installed = pkg
239
+ ? {
240
+ id: pkg.id,
241
+ name: pkg.name,
242
+ release: pkg.release,
243
+ requiredApps: pkg.requirements.apps.map((app) => ({ ...app })),
244
+ source: org ? "org" : "file",
245
+ installId,
246
+ agentKey: member.key,
247
+ ...(agent?.approval ? { suggestedApproval: agent.approval } : {}),
248
+ ...(org ? { publisher: { ...org.publisher }, ref: org.ref, sha256: org.sha256 } : {}),
249
+ }
250
+ : undefined;
251
+ // composio: false — a shared persona never starts with reach into the
252
+ // person's connected apps (absence would mean allowed). Approval is
253
+ // never set: every imported bot starts on Ask, whatever was suggested.
254
+ store.patchBot(created.id, {
255
+ composio: false,
256
+ connectorTools: {},
257
+ ...(playbooks.length ? { playbooks } : {}),
258
+ ...(installed ? { installedPackage: installed } : {}),
259
+ ...(agent?.connections ? { mcpServers: agent.connections.flatMap((key) => serverForKey.has(key) ? [serverForKey.get(key)] : []) } : {}),
260
+ ...avatar,
261
+ });
262
+ for (const skillName of agent?.skills ?? []) {
263
+ const skill = skillByName.get(skillName);
264
+ if (!skill)
265
+ throw new Error(`Package skill "${skillName}" is unavailable`);
266
+ // A file's `source` is display provenance only; the organization
267
+ // channel names its own source so ownership can never be forged.
268
+ // Organization skills arrive switched on, stamped with their install.
269
+ const added = org
270
+ ? deps.skills.installOrg(created.id, `org:${org.ref}@${pkg.release}`, skill.instructions, { installId: org.installId, key: skill.name, release: pkg.release, ...pair(skill.instructions, skill.instructions) })
271
+ : deps.skills.install(created.id, skill.source ?? `package:${pkg.id}`, [{ path: "SKILL.md", content: skill.instructions }]);
272
+ if ("error" in added)
273
+ throw new Error(`Package skill "${skillName}" could not be imported: ${added.error}`);
274
+ }
275
+ // Starter notes are copied once, on this first add.
276
+ for (const [path, text] of Object.entries(agent?.seed?.memory ?? {})) {
277
+ if (path === "MEMORY.md")
278
+ deps.memory.writeIndex(created.id, text);
279
+ else
280
+ deps.memory.writeTopic(created.id, path.slice("memory/".length), text);
281
+ notes += 1;
282
+ }
283
+ // An organization bot's part hashes are its last write, so a bot that
284
+ // has them is complete (org-library.ts reads that after a crash).
285
+ if (org && agent) {
286
+ const written = store.bot(created.id);
287
+ const local = {
288
+ name: written.name,
289
+ title: written.title ?? "",
290
+ description: written.description ?? "",
291
+ soul: written.soul ?? "",
292
+ look: {
293
+ color: written.color,
294
+ mascotExpression: written.mascotExpression ?? null,
295
+ mascotBody: written.mascotBody ?? null,
296
+ avatar: avatarHash,
297
+ crop: written.avatarCrop ?? null,
298
+ },
299
+ playbooks: playbookValues(written.playbooks ?? []),
300
+ skills: [...(agent.skills ?? [])].sort(),
301
+ connections: [...(written.mcpServers ?? [])].sort(),
302
+ approval: "ask",
303
+ };
304
+ store.patchBot(created.id, { packageBase: pairs(AGENT_PARTS, agentReleaseValues(agent, playbookByKey), local) });
305
+ orgIndex.bots[member.key] = created.id;
306
+ }
307
+ }
308
+ // The brief goes before the group chats and routines, and the leader is
309
+ // set last, so an organization Add the app stopped partway through can
310
+ // tell a finished team from a partial one (org-library.ts).
311
+ const brief = Boolean(pkg?.team?.brief?.trim());
312
+ if (brief)
313
+ deps.sections.writeBrief(section, pkg.team.brief);
314
+ // A package is an explicit structure import: its rooms are created from
315
+ // package-local keys only, then normalized to the fresh bot ids.
316
+ const groupIds = new Map();
317
+ for (const room of pkg?.rooms ?? []) {
318
+ const ids = room.members.map((key) => botIds.get(key));
319
+ let created = store.createGroup(room.name, ids, false, section);
320
+ createdGroups.push(created);
321
+ const defaultResponder = room.defaultResponder.kind === "agent"
322
+ ? { kind: "member", botId: botIds.get(room.defaultResponder.agent) }
323
+ : { kind: room.defaultResponder.kind };
324
+ created = store.patchGroup(created.id, {
325
+ bulletin: room.bulletin ?? "",
326
+ defaultResponder,
327
+ setupCompletedAt: Date.now(),
328
+ }) ?? created;
329
+ groupIds.set(room.key, created.id);
330
+ if (org) {
331
+ const local = {
332
+ name: created.name,
333
+ bulletin: created.bulletin ?? "",
334
+ members: [...created.memberIds].sort(),
335
+ defaultResponder: created.defaultResponder,
336
+ };
337
+ created = store.patchGroup(created.id, {
338
+ installedPackage: { installId: org.installId, key: room.key, memberKeys: [...room.members].sort(), parts: pairs(ROOM_PARTS, roomReleaseValues(room), local) },
339
+ }) ?? created;
340
+ orgIndex.rooms[room.key] = created.id;
341
+ }
342
+ }
343
+ for (const routine of pkg?.routines ?? []) {
344
+ // Routines always arrive paused from a file. The organization channel
345
+ // also pauses them for now; honouring enabledAfterInstall there is a
346
+ // later, Admin-side decision.
347
+ let timeoutMinutes = routine.timeoutMinutes;
348
+ if (timeoutMinutes !== undefined && (timeoutMinutes < 5 || timeoutMinutes > 240)) {
349
+ timeoutMinutes = Math.min(240, Math.max(5, timeoutMinutes));
350
+ skipped.push({ part: `routines[${routine.key}].timeoutMinutes`, reason: "run_limit_adjusted" });
351
+ }
352
+ const input = {
353
+ name: routine.name,
354
+ prompt: routine.prompt,
355
+ botId: botIds.get(routine.agent),
356
+ ...(routine.room !== undefined ? { target: "room-goal", groupId: groupIds.get(routine.room) } : {}),
357
+ runOn: routine.runOn,
358
+ enabled: false,
359
+ schedule: routine.schedule,
360
+ durationMinutes: routine.durationMinutes,
361
+ ...(timeoutMinutes === undefined ? {} : { timeoutMinutes }),
362
+ ...(routine.overlap ? { overlap: routine.overlap } : {}),
363
+ ...(routine.continuity && routine.room === undefined ? { continuity: true } : {}),
364
+ };
365
+ const created = deps.routines.create(input);
366
+ createdRoutines.push(created);
367
+ if (org) {
368
+ const local = {
369
+ name: created.name,
370
+ prompt: created.prompt,
371
+ schedule: scheduleValue(created),
372
+ target: { botId: created.botId, groupId: created.groupId ?? null },
373
+ };
374
+ deps.routines.stampInstalledPackage(created.id, { installId: org.installId, key: routine.key, parts: pairs(ROUTINE_PARTS, routineReleaseValues(routine), local) });
375
+ orgIndex.routines[routine.key] = created.id;
376
+ }
377
+ }
378
+ if (pkg?.team?.leader)
379
+ store.setChiefOfStaff(botIds.get(pkg.team.leader));
380
+ if (org && pkg?.team) {
381
+ orgIndex.section = section;
382
+ orgIndex.team.parts = pairs(TEAM_PARTS, {
383
+ name: pkg.team.name, brief: pkg.team.brief ?? "", leader: pkg.team.leader ?? null,
384
+ }, {
385
+ name: section, brief: brief ? pkg.team.brief : "", leader: pkg.team.leader ? botIds.get(pkg.team.leader) : null,
386
+ });
387
+ }
388
+ let presets;
389
+ // An organization install always registers, so a release re-added without
390
+ // presets also drops the rows an earlier one left (presets.ts).
391
+ if (source.kind === "package" && deps.presets && (pkg?.presets?.length || org)) {
392
+ const registered = deps.presets.register(source.document, { source: org ? "org" : "file", installId: installId, ...(org ? { org } : {}) });
393
+ createdPresets.push(...registered.added.map((preset) => preset.id));
394
+ skipped.push(...registered.skipped);
395
+ presets = [...registered.added, ...registered.existing];
396
+ }
397
+ else
398
+ for (const preset of pkg?.presets ?? [])
399
+ skipped.push({ part: `presets[${preset.key}]`, reason: "presets_not_supported_yet" });
400
+ // The legacy project room is created last, so a failure anywhere above
401
+ // leaves no half-built project behind.
402
+ if (!pkg && options.mode === "project" && importedBots.length > 0) {
403
+ const roomName = options.room?.trim() || name;
404
+ group = store.createGroup(roomName, importedBots.map((bot) => bot.id), false, section);
405
+ createdGroups.push(group);
406
+ if (options.cwd) {
407
+ // `cwd` is the folder the room WANTS; the store pins it on the
408
+ // first turn (pinGroupCwd).
409
+ group = store.patchGroup(group.id, { cwd: options.cwd }) ?? group;
410
+ }
411
+ }
412
+ const referenced = new Set((pkg?.agents ?? []).flatMap((agent) => agent.skills ?? []));
413
+ const bots = importedBots.map((bot) => store.bot(bot.id));
414
+ for (const bot of bots)
415
+ deps.broadcast?.({ kind: "bot", bot });
416
+ if (group)
417
+ deps.broadcast?.({ kind: "group", group });
418
+ return {
419
+ alreadyAdded: false,
420
+ ...(installId ? { installId } : {}),
421
+ ...(org ? { org: orgIndex } : {}),
422
+ name,
423
+ section,
424
+ bots,
425
+ groups: createdGroups.map((created) => store.group(created.id) ?? created),
426
+ ...(group ? { group } : {}),
427
+ routines: createdRoutines,
428
+ offeredSkills: (pkg?.skills?.entries ?? []).filter((skill) => !referenced.has(skill.name)).map((skill) => skill.name),
429
+ connections,
430
+ skipped,
431
+ brief,
432
+ notes,
433
+ ...(presets ? { presets } : {}),
434
+ };
435
+ }
436
+ catch (error) {
437
+ // A room of deleted members must not survive either — patchGroup can
438
+ // throw (disk) after createGroup already saved.
439
+ for (const routine of createdRoutines)
440
+ deps.routines.remove(routine.id);
441
+ for (const created of createdGroups)
442
+ store.deleteGroup(created.id);
443
+ for (const bot of importedBots)
444
+ store.deleteBot(bot.id);
445
+ deps.presets?.remove(createdPresets);
446
+ if (createdServers.length) {
447
+ const next = { ...deps.mcp.servers() };
448
+ for (const serverName of createdServers)
449
+ delete next[serverName];
450
+ deps.mcp.persist(next);
451
+ }
452
+ // This import allocated a fresh section (and maybe its brief); a failed
453
+ // installation retires that identity again.
454
+ if (section && store.sections.includes(section))
455
+ store.changeEmptySection(section, null);
456
+ throw error;
457
+ }
458
+ }
@@ -0,0 +1,87 @@
1
+ // Part hashes for records added from an organization's library (contract
2
+ // §1.6). Every part of an added team keeps two hashes: `r`, the value the
3
+ // release shipped, and `w`, the value OpenMausBot wrote at install. The
4
+ // automatic updater compares them later: `hash(local) ≠ w` means the person
5
+ // edited the part, `r(new release) ≠ r` means the publisher changed it. Only
6
+ // the hashes are kept, never a second copy of the content.
7
+ //
8
+ // The value shapes are frozen: a stamp written today must still compare
9
+ // correctly in a later version, so change nothing here without a new stamp
10
+ // version.
11
+ import { createHash } from "node:crypto";
12
+ import { canonicalJson, decodeBase64 } from "../shared/package-format.js";
13
+ export const AGENT_PARTS = ["name", "title", "description", "soul", "look", "playbooks", "skills", "connections", "approval"];
14
+ export const ROOM_PARTS = ["name", "bulletin", "members", "defaultResponder"];
15
+ export const ROUTINE_PARTS = ["name", "prompt", "schedule", "target"];
16
+ export const TEAM_PARTS = ["name", "brief", "leader"];
17
+ export const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
18
+ /** sha256hex(utf8(canonicalJson(value ?? null))) */
19
+ export function partHash(value) {
20
+ return sha256Hex(canonicalJson(value ?? null));
21
+ }
22
+ export const pair = (release, written) => ({ r: partHash(release), w: partHash(written) });
23
+ export function releaseLook(appearance) {
24
+ const bytes = appearance.avatar ? decodeBase64(appearance.avatar.data) : null;
25
+ return {
26
+ color: appearance.color,
27
+ mascotExpression: appearance.mascotExpression ?? null,
28
+ mascotBody: appearance.mascotBody ?? null,
29
+ avatar: bytes ? sha256Hex(bytes) : null,
30
+ crop: appearance.avatar?.crop ?? null,
31
+ };
32
+ }
33
+ export function playbookValues(playbooks) {
34
+ return playbooks.map(({ key, name, summary, triggers, instructions }) => ({ key, name, summary, triggers: [...triggers], instructions }));
35
+ }
36
+ /** The release side of every bot part. */
37
+ export function agentReleaseValues(agent, playbooks) {
38
+ return {
39
+ name: agent.name,
40
+ title: agent.title ?? "",
41
+ description: agent.description ?? "",
42
+ soul: agent.soul ?? "",
43
+ look: releaseLook(agent.appearance),
44
+ playbooks: playbookValues((agent.playbooks ?? []).flatMap((key) => (playbooks.has(key) ? [playbooks.get(key)] : []))),
45
+ skills: [...(agent.skills ?? [])].sort(),
46
+ connections: [...(agent.connections ?? [])].sort(),
47
+ approval: agent.approval ?? null,
48
+ };
49
+ }
50
+ export const roomReleaseValues = (room) => ({
51
+ name: room.name,
52
+ bulletin: room.bulletin ?? "",
53
+ members: [...room.members].sort(),
54
+ defaultResponder: room.defaultResponder.kind === "agent"
55
+ ? { kind: "agent", agent: room.defaultResponder.agent }
56
+ : { kind: room.defaultResponder.kind },
57
+ });
58
+ /** One normal form for a routine's timing on both sides, so an untouched
59
+ * routine compares equal: absent cap → null, absent overlap → "skip". */
60
+ export function scheduleValue(routine) {
61
+ return {
62
+ runOn: routine.runOn,
63
+ schedule: routine.schedule,
64
+ durationMinutes: routine.durationMinutes,
65
+ timeoutMinutes: routine.timeoutMinutes ?? null,
66
+ overlap: routine.overlap ?? "skip",
67
+ continuity: routine.continuity === true,
68
+ };
69
+ }
70
+ export const routineReleaseValues = (routine) => ({
71
+ name: routine.name,
72
+ prompt: routine.prompt,
73
+ schedule: scheduleValue(routine),
74
+ target: { agent: routine.agent, room: routine.room ?? null },
75
+ });
76
+ export const connectionReleaseValue = (connection) => ({
77
+ transport: connection.mcp.transport,
78
+ url: connection.mcp.url,
79
+ valueNames: [...connection.mcp.valueNames],
80
+ });
81
+ export function connectionLocalValue(server) {
82
+ const headers = server.headers && typeof server.headers === "object" ? Object.keys(server.headers) : [];
83
+ return { transport: server.type ?? null, url: server.url ?? null, valueNames: headers.sort() };
84
+ }
85
+ export function pairs(keys, release, written) {
86
+ return Object.fromEntries(keys.map((key) => [key, pair(release[key], written[key])]));
87
+ }
@@ -17,6 +17,15 @@ import { newId } from "./contracts.js";
17
17
  import { buildNotification } from "./notify.js";
18
18
  import { peerAllowKey } from "./peer-approval-key.js";
19
19
  export { peerAllowKey } from "./peer-approval-key.js";
20
+ export function peerApprovalFailure(outcome) {
21
+ return {
22
+ error: outcome === "deny" ? "denied by user"
23
+ : outcome === "expired" ? "the approval card expired without an answer"
24
+ : "the approval was cancelled before a decision",
25
+ approvalOutcome: outcome,
26
+ approvalSource: outcome === "deny" ? "user" : "system",
27
+ };
28
+ }
20
29
  /** Mark the card answered so the UI stops treating it as pending. Mirrors
21
30
  * what the `request.resolved` fold does for provider cards; a harness-native
22
31
  * card never emits that event, so it has to settle itself. */
@@ -102,7 +111,7 @@ function announceCard(bus, from, card, sourceThreadId) {
102
111
  bus.notify?.(buildNotification("approval", from, sourceThreadId, detail, { avatarUrl: from.avatarUrl, group }));
103
112
  }
104
113
  /** Ask the user (in the source task thread) whether `from` may `action` `target`.
105
- * Resolves with `"allow"` or `"deny"`. If `from.alwaysAllow` already
114
+ * Distinguishes user denial from expiry and cancellation. If `from.alwaysAllow` already
106
115
  * covers the (action, target) pair, returns `"allow"` immediately
107
116
  * without a card. */
108
117
  export function requestPeerApproval(bus, from, target, message, action, sourceThreadId = from.threadId) {
@@ -116,14 +125,14 @@ export function requestPeerApproval(bus, from, target, message, action, sourceTh
116
125
  const card = pushApprovalCard(bus, from, target, message, action, requestId, sourceThreadId);
117
126
  announceCard(bus, from, card, sourceThreadId);
118
127
  const timer = setTimeout(() => {
119
- // 15 minutes without an answer → deny. Keeps an unattended bot from
128
+ // 15 minutes without an answer → expired. Keeps an unattended bot from
120
129
  // stalling its own turn forever (matches the Claude broker timeout).
121
130
  const pending = pendingComms.get(requestId);
122
131
  if (!pending)
123
132
  return;
124
133
  pendingComms.delete(requestId);
125
134
  settleCard(pending, "deny", "system");
126
- resolve("deny");
135
+ resolve("expired");
127
136
  }, APPROVAL_TIMEOUT_MS);
128
137
  timer.unref?.(); // a waiting card must never hold the process open
129
138
  pendingComms.set(requestId, {
@@ -154,7 +163,7 @@ export function resolvePeerComms(_bus, requestId, behavior) {
154
163
  return true;
155
164
  }
156
165
  /** Drop every approval waiting on a bot that no longer exists (or is being
157
- * deleted), denying it so the caller's turn doesn't wait out the timeout. */
166
+ * deleted), cancelling it so the caller's turn doesn't wait out the timeout. */
158
167
  export function cancelPeerApprovalsFor(botId) {
159
168
  for (const [requestId, pending] of Array.from(pendingComms)) {
160
169
  if (pending.fromBotId !== botId && pending.toBotId !== botId)
@@ -162,10 +171,10 @@ export function cancelPeerApprovalsFor(botId) {
162
171
  pendingComms.delete(requestId);
163
172
  clearTimeout(pending.timer);
164
173
  settleCard(pending, "deny", "system");
165
- pending.resolve("deny");
174
+ pending.resolve("cancelled");
166
175
  }
167
176
  }
168
- /** Deny every peer-communication approval owned by a thread whose turn was
177
+ /** Cancel every peer-communication approval owned by a thread whose turn was
169
178
  * interrupted. Patching the card alone is not enough: the in-memory promise
170
179
  * must resolve too, or the delegation queue waits until its 15-minute timer. */
171
180
  export function cancelPeerApprovalsForThread(threadId) {
@@ -175,7 +184,7 @@ export function cancelPeerApprovalsForThread(threadId) {
175
184
  pendingComms.delete(requestId);
176
185
  clearTimeout(pending.timer);
177
186
  settleCard(pending, "deny", "system");
178
- pending.resolve("deny");
187
+ pending.resolve("cancelled");
179
188
  }
180
189
  }
181
190
  /** Every thread a peer card can be raised in. A card lands in the thread
@@ -3,6 +3,7 @@
3
3
  // every other bot now gets are rendered from here, so there is one set of
4
4
  // caps, one sanitizer, and one reachability rule to audit rather than two
5
5
  // that drift.
6
+ import { sameAudience } from "./bot-visibility.js";
6
7
  const sectionKey = (section) => section?.trim() || "";
7
8
  export const PEER_ACCESS_HELP = "Call list_bots for reachable teammates. If the intended Chief is missing, ask the user to check team membership and this bot's allowed peers, or message the Chief directly. A Chief's access to another team does not grant that team's bots access back to the Chief. Do not use computer control to bypass this.";
8
9
  /** Coordination is scoped to the bot's own team unless the owner explicitly
@@ -13,6 +14,16 @@ export function canAccessTeam(from, section) {
13
14
  return target === sectionKey(from.section) || Boolean(from.chiefOfStaff &&
14
15
  Array.isArray(from.managedSections) && from.managedSections.some(value => typeof value === "string" && sectionKey(value) === target));
15
16
  }
17
+ /** Returns whether `coordinator` is an authorized Chief of Staff supervising `bot`'s section. */
18
+ export function coordinatorSupervises(coordinator, bot) {
19
+ if (!coordinator?.chiefOfStaff || !bot)
20
+ return false;
21
+ const target = sectionKey(bot.section);
22
+ if (!target)
23
+ return false;
24
+ return Boolean(Array.isArray(coordinator.managedSections) &&
25
+ coordinator.managedSections.some((value) => typeof value === "string" && sectionKey(value) === target));
26
+ }
16
27
  const PEER_STATUS_WORDS = {
17
28
  available: "available",
18
29
  working: "working right now",
@@ -51,9 +62,19 @@ export function peerStatusWords(status) {
51
62
  * written by an older build) falls back to the unset rule rather than
52
63
  * throwing mid-turn: the list is operator-owned local state, so degrading to
53
64
  * the documented default is safer than failing a turn. */
54
- export const peerAllowed = (from, targetId) => !Array.isArray(from.peers) || from.peers.includes(targetId);
65
+ export const peerAllowed = (from, target) => {
66
+ const targetId = typeof target === "string" ? target : target.id;
67
+ if (Array.isArray(from.peers) && !from.peers.includes(targetId))
68
+ return false;
69
+ // Given the record, also require the same audience: a teammate that other
70
+ // people can see would carry this bot's words, or bring back a restricted
71
+ // bot's answers, to people who cannot see the other (bot-visibility.ts).
72
+ // Bots nobody restricted all share "everyone", so this changes nothing
73
+ // until an admin restricts one.
74
+ return typeof target === "string" || sameAudience(from.visibility, target.visibility);
75
+ };
55
76
  export function canReachPeer(from, target) {
56
- return from.id !== target.id && !target.hidden && canAccessTeam(from, target.section) && peerAllowed(from, target.id);
77
+ return from.id !== target.id && !target.hidden && canAccessTeam(from, target.section) && peerAllowed(from, target);
57
78
  }
58
79
  /** The peers a bot can both see and reach right now. The roster, list_bots
59
80
  * and @mention resolution all read this one list, so what a bot is TOLD