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
@@ -2,6 +2,83 @@ import { timingSafeEqual } from "node:crypto";
2
2
  import { isIP } from "node:net";
3
3
  import { denyReason as companionDenial } from "../companion/src/routes.js";
4
4
  const LOOPBACK_SCOPES = ["admin", "client"];
5
+ const SERVICE_SCOPES = ["client"];
6
+ /** What a `service`-trust loopback caller may reach, and nothing else:
7
+ *
8
+ * - liveness and identity: health, who-am-I, edition and brand;
9
+ * - the cloud Slack worker (openmaus-cloud server/slack-worker.ts), which
10
+ * shares the workspace's network namespace and reads the bot list, a
11
+ * thread's messages and a bot's PNG picture, creates a thread, sends
12
+ * through the guarded route, watches and stops its exact request,
13
+ * withdraws a queued line, and declines a card (the handler refuses any
14
+ * other answer from this caller);
15
+ * - the turn-scoped bot capability routes, whose own bearer token is the
16
+ * authorization (checked in the handler), and the test-only capability
17
+ * mint that exists only when its private key is set.
18
+ *
19
+ * Residual risk, not closed here: a bot's shell is a loopback caller too, so
20
+ * it can do everything the worker does. It can post into any bot's thread
21
+ * through the guarded route, including an existing Full-access thread
22
+ * (`expectedApprovalMode: "full"`), and while the operator's shared Full
23
+ * access is on it can open new Full-access threads; either way work runs with
24
+ * Full access and no card, whoever asked the bot. It can also stop a request
25
+ * and decline a card. It can no longer change settings, keys, instances, MCP
26
+ * servers, webhooks, sessions, people, budgets or fleet, loosen a bot's
27
+ * permissions, or approve anything. The planned fix is a relay token that only
28
+ * the Slack worker holds, so these routes stop answering session-less
29
+ * loopback at all. */
30
+ export const SERVICE_ALLOW = [
31
+ { methods: ["GET"], path: /^\/api\/health$/ },
32
+ { methods: ["GET"], path: /^\/api\/auth\/session$/ },
33
+ { methods: ["GET"], path: /^\/api\/edition$/ },
34
+ { methods: ["GET"], path: /^\/api\/brand$/ },
35
+ { methods: ["GET"], path: /^\/api\/bots$/ },
36
+ { methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages$/ },
37
+ { methods: ["GET"], path: /^\/api\/attachments\/[\w.-]+$/ },
38
+ { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks$/ },
39
+ { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/messages\/guarded$/ },
40
+ { methods: ["GET"], path: /^\/api\/bots\/[\w-]+\/requests\/[\w-]+$/ },
41
+ { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/requests\/[\w-]+\/interrupt$/ },
42
+ { methods: ["DELETE"], path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
43
+ { methods: ["POST"], path: /^\/api\/threads\/[\w-]+\/respond$/ }, // decline only: see the handler
44
+ { methods: ["GET", "POST", "PUT", "PATCH", "DELETE"], path: /^\/api\/internal\// }, // capability bearer checked in the handler
45
+ { methods: ["POST"], path: /^\/api\/testing\/internal-capability$/ }, // exists only with its private key
46
+ ];
47
+ export function serviceAllowed(method, path) {
48
+ const upper = method.toUpperCase();
49
+ return SERVICE_ALLOW.some((rule) => rule.methods.includes(upper) && rule.path.test(path));
50
+ }
51
+ /** Pick the loopback trust for this process, and say why for the startup log.
52
+ *
53
+ * A packaged desktop keeps `owner`: its mutations already need Electron's
54
+ * per-launch capability, and only its owner uses the machine. Elsewhere the
55
+ * operator may set OMB_LOOPBACK_TRUST=owner|service. Without it a hosted
56
+ * workspace (any OMB_ADMIN_* setting, even an incomplete one) defaults to
57
+ * `service` — shared-workspace Full access is only honoured there, so it needs
58
+ * no rule of its own — and a headless self-hosted server keeps `owner`. A
59
+ * value that is neither fails closed. */
60
+ export function resolveLoopbackTrust(input) {
61
+ const raw = (input.env ?? process.env).OMB_LOOPBACK_TRUST;
62
+ const requested = raw?.trim().toLowerCase();
63
+ if (input.desktopManaged) {
64
+ return { trust: "owner", reason: "desktop app", ...(raw !== undefined ? { warning: "OMB_LOOPBACK_TRUST is ignored in the desktop app" } : {}) };
65
+ }
66
+ if (requested === "owner" || requested === "service") {
67
+ return {
68
+ trust: requested,
69
+ reason: "OMB_LOOPBACK_TRUST",
70
+ ...(requested === "owner" && input.hostedWorkspace
71
+ ? { warning: "OMB_LOOPBACK_TRUST=owner on a shared workspace: every bot's shell can change settings and approve cards as the owner" }
72
+ : {}),
73
+ };
74
+ }
75
+ if (raw !== undefined && requested !== "") {
76
+ return { trust: "service", reason: "OMB_LOOPBACK_TRUST", warning: `OMB_LOOPBACK_TRUST="${raw.replace(/[^\w.-]/g, "").slice(0, 40)}" is not owner or service; using service` };
77
+ }
78
+ if (input.hostedWorkspace)
79
+ return { trust: "service", reason: "hosted workspace" };
80
+ return { trust: "owner", reason: "self-hosted default" };
81
+ }
5
82
  export function isLoopbackHost(host) {
6
83
  if (!host)
7
84
  return false;
@@ -170,6 +247,8 @@ export const CLIENT_ALLOW = [
170
247
  // reads: fleet, transcripts, search (no secrets in any of these)
171
248
  { methods: ["GET"], path: /^\/api\/bots$/ },
172
249
  { methods: ["GET"], path: /^\/api\/team-map$/ },
250
+ // a link into the organisation's Admin: identifiers only, and Admin authorizes its own visitor
251
+ { methods: ["GET"], path: /^\/api\/bots\/[\w-]+\/slack-management$/ },
173
252
  { methods: ["GET"], path: /^\/api\/search$/ },
174
253
  { methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages$/ },
175
254
  { methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/image$/ },
@@ -219,6 +298,7 @@ export const CLIENT_ALLOW = [
219
298
  { methods: ["PATCH", "DELETE"], path: /^\/api\/routines\/[\w-]+$/ },
220
299
  { methods: ["POST"], path: /^\/api\/routines\/[\w-]+\/run$/ },
221
300
  { methods: ["POST"], path: /^\/api\/routine-runs\/[\w-]+\/(?:cancel|seen)$/ },
301
+ { methods: ["POST"], path: /^\/api\/routine-runs\/seen-all$/ },
222
302
  // webhook list is secret-free; creating or rotating one is not
223
303
  { methods: ["GET"], path: /^\/api\/webhooks$/ },
224
304
  // configured-or-not booleans; the handler strips the few identifying fields for clients
@@ -255,6 +335,9 @@ export function clientGroupPatchViolation(body) {
255
335
  return key;
256
336
  return null;
257
337
  }
338
+ const CLI_OWNER_HEADER = "x-openmausbot-cli-owner";
339
+ /** The only routes the serving CLI's secret opens: its pairing code. */
340
+ const CLI_OWNER_ROUTE = /^\/api\/auth\/pairing$/;
258
341
  const DESKTOP_OWNER_HEADER = "x-openmausbot-desktop-owner";
259
342
  function mutatingPublicRoute(method, path) {
260
343
  const upper = method.toUpperCase();
@@ -343,6 +426,19 @@ export function resolveRequestAuth(req, options) {
343
426
  !secureTokenMatch(headerValue(req.headers[DESKTOP_OWNER_HEADER]), options.loopbackMutationToken)) {
344
427
  return deny(403, "forbidden: this change must come from the desktop app or a paired device");
345
428
  }
429
+ if (options.loopbackMutationToken === undefined && options.loopbackTrust === "service") {
430
+ // The CLI that started this server may still print a pairing code.
431
+ if (options.cliOwnerToken && CLI_OWNER_ROUTE.test(path) && ["GET", "POST"].includes(method.toUpperCase()) &&
432
+ secureTokenMatch(headerValue(req.headers[CLI_OWNER_HEADER]), options.cliOwnerToken)) {
433
+ return { auth: { kind: "loopback", scopes: LOOPBACK_SCOPES }, status: 401, error: "" };
434
+ }
435
+ // On a shared server "local" includes every bot's shell. Default deny:
436
+ // only the service routes, never an admin change, without a session.
437
+ if (!serviceAllowed(method, path)) {
438
+ return deny(403, "forbidden: on this shared server a local request without a session may only use the service routes; sign in to change settings, people or access");
439
+ }
440
+ return { auth: { kind: "loopback", scopes: SERVICE_SCOPES, trust: "service" }, status: 401, error: "" };
441
+ }
346
442
  return { auth: { kind: "loopback", scopes: LOOPBACK_SCOPES }, status: 401, error: "" };
347
443
  }
348
444
  if (proxied) {
@@ -6,6 +6,7 @@ const nodeSchema = z.object({
6
6
  id: z.string(), rootId: z.string(), parentId: z.string().optional(),
7
7
  groupId: z.string().optional(), threadId: z.string(), botId: z.string(),
8
8
  key: z.string(), text: z.string(), createdAt: z.number(),
9
+ requestBatchKey: z.string().optional(),
9
10
  status: z.enum(["source", "queued", "running", "waiting", "resume", "completed", "failed", "cancelled"]),
10
11
  result: z.string().default(""), reported: z.boolean().default(false),
11
12
  executions: z.number().int().nonnegative().default(0), startedAt: z.number().optional(),
@@ -159,7 +160,7 @@ export class RoomHandoffs {
159
160
  const root = this.root(n);
160
161
  return `Room handoff hard cap exhausted: node was ${n.status} after ${duration(this.now() - root.createdAt)} of the ${duration(this.limits.hardCapMs)} wall-clock cap`;
161
162
  }
162
- enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "") {
163
+ enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "", requestBatchKey) {
163
164
  if (this.loadError)
164
165
  throw new Error(this.loadError);
165
166
  let parent = parentId ? this.nodes.get(parentId) : this.nodes.get(generation);
@@ -178,10 +179,17 @@ export class RoomHandoffs {
178
179
  const existing = this.children(parent.id).find(n => n.key === key);
179
180
  if (existing) {
180
181
  if (existing.groupId !== target.groupId || existing.botId !== target.botId || existing.text !== text ||
181
- existing.kind !== kind)
182
+ existing.kind !== kind || existing.requestBatchKey !== requestBatchKey)
182
183
  throw new Error("request_key was already used for different work");
183
184
  return { node: existing, duplicate: true };
184
185
  }
186
+ if (requestBatchKey && target.groupId) {
187
+ const batch = this.children(parent.id).filter(n => n.requestBatchKey === requestBatchKey && n.groupId === target.groupId);
188
+ if (batch.some(n => n.text !== text || n.threadId !== target.threadId))
189
+ throw new Error("request_key was already used for different room work");
190
+ if (batch.some(n => n.startedAt !== undefined))
191
+ throw new Error("This shared room request has already started; use a new request_key for additional recipients");
192
+ }
185
193
  if (!rework && this.children(parent.id).some(n => n.kind === kind &&
186
194
  n.groupId === target.groupId && n.botId === target.botId && n.status === "completed")) {
187
195
  throw new Error("This agent already completed your assignment. Do not send acknowledgements or approvals as new work. Finish with your decision; results return automatically. Only use rework=true for concrete additional work.");
@@ -221,7 +229,7 @@ export class RoomHandoffs {
221
229
  }
222
230
  const node = { ...target, id: randomUUID(), rootId: parent.rootId, parentId: parent.id,
223
231
  key, text, createdAt: this.now(), status: "queued", result: "", reported: false, executions: 0, approvalGranted,
224
- kind };
232
+ kind, ...(target.groupId && requestBatchKey ? { requestBatchKey } : {}) };
225
233
  const problem = this.hooks.validate(node, parent);
226
234
  if (problem)
227
235
  throw new Error(problem);
@@ -239,16 +247,25 @@ export class RoomHandoffs {
239
247
  }
240
248
  return { node, duplicate: false };
241
249
  }
250
+ /** A room brief is displayed once; each recipient keeps its own execution and result. */
251
+ sharedRequest(node) {
252
+ const batch = node.groupId && node.requestBatchKey && node.parentId
253
+ ? this.children(node.parentId).filter(n => n.requestBatchKey === node.requestBatchKey &&
254
+ n.groupId === node.groupId && n.threadId === node.threadId && n.text === node.text)
255
+ : [node];
256
+ return { id: batch[0]?.id ?? node.id, botIds: batch.map(n => n.botId) };
257
+ }
242
258
  sourceSettled(generation, ok) {
243
259
  const node = this.nodes.get(generation);
244
260
  if (!node || node.status !== "source")
245
261
  return;
262
+ // An accepted assignment belongs to the queue, not the provider that
263
+ // submitted it. A failed/expired source turn must not erase that work.
264
+ // Explicit Stop, deletion and revoked routes still cancel separately.
246
265
  if (!ok)
247
- this.cancelTree(node, "The originating room turn did not finish", "failed");
248
- else {
249
- node.status = "waiting";
250
- this.publish(node);
251
- }
266
+ node.result = "The originating turn ended before its teammates returned.";
267
+ node.status = "waiting";
268
+ this.publish(node);
252
269
  }
253
270
  cancelTree(node, reason, status = "cancelled") {
254
271
  for (const child of this.children(node.id))
@@ -347,8 +364,10 @@ export class RoomHandoffs {
347
364
  }
348
365
  if (n.status !== "queued" && n.status !== "resume")
349
366
  continue;
350
- // A newly queued child starts only after its author has settled.
351
- if (parent && (parent.status === "source" || parent.status === "running"))
367
+ // Independent conversations can start as soon as work is accepted.
368
+ // Same-room speakers still serialize; never overlap their shared chat.
369
+ if (parent && (parent.status === "source" || parent.status === "running") &&
370
+ (parent.threadId === n.threadId || (n.groupId && n.groupId === parent.groupId)))
352
371
  continue;
353
372
  // A stopped source stops waiting; only work that never started is
354
373
  // dropped with it. A teammate mid-turn keeps its process and reports.
@@ -381,10 +400,10 @@ export class RoomHandoffs {
381
400
  if (terminal(n))
382
401
  return;
383
402
  n.result = result.text.slice(0, 12_000);
384
- if (!result.ok)
385
- this.cancelTree(n, n.result || "Room agent failed", "failed");
386
- else if (this.children(n.id).length > childCount)
403
+ if (this.children(n.id).length > childCount)
387
404
  n.status = "waiting";
405
+ else if (!result.ok)
406
+ this.cancelTree(n, n.result || "Room agent failed", "failed");
388
407
  else
389
408
  n.status = "completed";
390
409
  // Close the paused span with the settlement itself: work enqueued
@@ -392,7 +411,18 @@ export class RoomHandoffs {
392
411
  // budget, not the still-open pause's overstated runway.
393
412
  this.trackExecutionPauses();
394
413
  this.publish(n);
395
- }).catch(e => { this.cancelTree(n, String(e).slice(0, 1000), "failed"); })
414
+ }).catch(e => {
415
+ if (terminal(n))
416
+ return;
417
+ n.result = String(e).slice(0, 1000);
418
+ if (this.children(n.id).length > childCount) {
419
+ n.status = "waiting";
420
+ this.trackExecutionPauses();
421
+ this.publish(n);
422
+ }
423
+ else
424
+ this.cancelTree(n, n.result, "failed");
425
+ })
396
426
  .finally(() => this.controllers.delete(n.id));
397
427
  }
398
428
  }
@@ -0,0 +1,25 @@
1
+ // Preset bots for New bot (server/presets.ts has the store and the rules).
2
+ // GET lists what New bot offers: the organization's presets first, then
3
+ // imported files', never one from a withdrawn release. DELETE removes an
4
+ // imported preset (the person's own click; no confirm step). Organization
5
+ // presets are Admin's to remove. Both are admin-scoped by default in
6
+ // server/request-auth.ts, like the New bot defaults: a preset adds skills
7
+ // and starter notes to the bot made from it.
8
+ import { listBotPresets, ORG_PRESET_REMOVE_MESSAGE, PRESET_UNAVAILABLE_MESSAGE } from "../presets.js";
9
+ import { PASS } from "./table.js";
10
+ export function createBotPresetRoutes(deps) {
11
+ return async ({ res, path, method, json }) => {
12
+ if (path === "/api/bot-presets" && method === "GET") {
13
+ return json(res, 200, { presets: listBotPresets(deps.presets, deps.orgStatuses()) });
14
+ }
15
+ const m = path.match(/^\/api\/bot-presets\/([\w-]+)$/);
16
+ if (!m || method !== "DELETE")
17
+ return PASS;
18
+ const removed = deps.presets.removeFilePreset(m[1]);
19
+ if (removed === "not_found")
20
+ return json(res, 404, { error: PRESET_UNAVAILABLE_MESSAGE });
21
+ if (removed === "organization")
22
+ return json(res, 409, { error: ORG_PRESET_REMOVE_MESSAGE });
23
+ return json(res, 200, { ok: true });
24
+ };
25
+ }
@@ -0,0 +1,20 @@
1
+ // GET /api/bots/:id/slack-management: where, in the organisation's Admin, this
2
+ // bot's Slack app is managed. A deep link only: this server holds no Slack
3
+ // logic, contacts nobody, and the link carries identifiers, never a credential.
4
+ // Client-scoped in server/request-auth.ts, so a member reading Bot Settings
5
+ // gets the same link; Admin authorizes whoever opens it.
6
+ import { hostedSlackManagement } from "../hosted-slack.js";
7
+ import { PASS } from "./table.js";
8
+ export function createHostedSlackRoutes(deps) {
9
+ return async ({ res, path, method, json }) => {
10
+ const m = path.match(/^\/api\/bots\/([\w-]+)\/slack-management$/);
11
+ if (!m || method !== "GET")
12
+ return PASS;
13
+ const bot = deps.bot(m[1]);
14
+ if (!bot || bot.hidden)
15
+ return json(res, 404, { error: "no such bot" });
16
+ res.setHeader("cache-control", "no-store");
17
+ // The id in the link is the stored bot's, not the text from the request.
18
+ return json(res, 200, hostedSlackManagement(bot.id, deps.hostedReady(), deps.env));
19
+ };
20
+ }
@@ -0,0 +1,14 @@
1
+ /** "Not my route": the next handler, then index.ts's inline routes, get a turn. */
2
+ export const PASS = Symbol("route.pass");
3
+ export const ROUTES = [];
4
+ /** Runs handlers in order until one answers; true means stop routing. A
5
+ * handler that wrote a response but returned PASS by mistake still counts as
6
+ * answered, so the request can never reach a second handler. */
7
+ export async function dispatchRoutes(routes, ctx) {
8
+ for (const route of routes) {
9
+ const out = await route(ctx);
10
+ if (out !== PASS || ctx.res.headersSent || ctx.res.writableEnded)
11
+ return true;
12
+ }
13
+ return false;
14
+ }
@@ -923,7 +923,7 @@ export class RoutineRequestService {
923
923
  if (refusal)
924
924
  throw new RoutineRequestError(refusal, 403);
925
925
  }
926
- await this.requireCloudReadiness(operation);
926
+ await this.requireCloudReadiness(operation, botId);
927
927
  // The readiness probe is asynchronous. Another request can edit or
928
928
  // delete the routine while it is in flight, so re-check the captured
929
929
  // revision before rendering and persisting the confirmation snapshot.
@@ -1006,7 +1006,7 @@ export class RoutineRequestService {
1006
1006
  }
1007
1007
  throw new RoutineRequestError(result.state === "invalid" ? result.error : "The routine change could not be applied", result.state === "invalid" ? result.status : 409);
1008
1008
  }
1009
- async requireCloudReadiness(operation) {
1009
+ async requireCloudReadiness(operation, proposerBotId) {
1010
1010
  if (!this.cloudReady || operation.action === "pause" || operation.action === "delete")
1011
1011
  return;
1012
1012
  const definition = effectiveDefinition(operation, this.routines);
@@ -1014,7 +1014,10 @@ export class RoutineRequestService {
1014
1014
  return;
1015
1015
  let readiness;
1016
1016
  try {
1017
- readiness = await this.cloudReady();
1017
+ const targetBotId = operation.action === "create"
1018
+ ? operation.forBot?.botId ?? proposerBotId
1019
+ : this.routines.listRoutines().find(routine => routine.id === operation.routineId)?.botId ?? proposerBotId;
1020
+ readiness = await this.cloudReady(targetBotId);
1018
1021
  }
1019
1022
  catch (error) {
1020
1023
  const detail = error instanceof Error ? error.message : String(error);
@@ -6,7 +6,25 @@ import { DATA_DIR } from "./config.js";
6
6
  import { writeFileAtomic } from "./atomic.js";
7
7
  import { redactSecretsInText } from "./redact.js";
8
8
  import { normalizeCronSchedule, nextCronRuns } from "../shared/routine-schedule.js";
9
+ import { isRoutineProblemRun } from "../shared/routines.js";
10
+ import { ROUTINE_PARTS } from "./package-parts.js";
9
11
  const persistedSourceThreadId = z.string().trim().min(1).optional().catch(undefined);
12
+ const HASH = /^[a-f0-9]{64}$/;
13
+ function loadInstalledPackage(value) {
14
+ if (!value || typeof value !== "object")
15
+ return undefined;
16
+ const stamp = value;
17
+ if (typeof stamp.installId !== "string" || !/^[a-f0-9]{32}$/.test(stamp.installId) || typeof stamp.key !== "string" || !stamp.key || stamp.key.length > 80)
18
+ return undefined;
19
+ const parts = stamp.parts;
20
+ if (!parts || typeof parts !== "object" || !ROUTINE_PARTS.every((part) => HASH.test(String(parts[part]?.r)) && HASH.test(String(parts[part]?.w))))
21
+ return undefined;
22
+ return {
23
+ installId: stamp.installId,
24
+ key: stamp.key,
25
+ parts: Object.fromEntries(ROUTINE_PARTS.map((part) => [part, { r: parts[part].r, w: parts[part].w }])),
26
+ };
27
+ }
10
28
  const webhookRunReceiptSchema = z.object({
11
29
  webhookId: z.string().min(1).max(200),
12
30
  deliveryId: z.string().min(1).max(200),
@@ -489,9 +507,12 @@ export class RoutineManager {
489
507
  overlap: routine.overlap === "queue" ? "queue" : undefined,
490
508
  skippedRuns: Number.isSafeInteger(routine.skippedRuns) && routine.skippedRuns > 0 ? routine.skippedRuns : undefined,
491
509
  lastSkippedAt: Number.isSafeInteger(routine.lastSkippedAt) && routine.lastSkippedAt >= 0 && routine.lastSkippedAt <= MAX_DATE_MS ? routine.lastSkippedAt : undefined,
510
+ installedPackage: loadInstalledPackage(routine.installedPackage),
492
511
  };
493
512
  if (loaded.timeoutMinutes === undefined)
494
513
  delete loaded.timeoutMinutes;
514
+ if (loaded.installedPackage === undefined)
515
+ delete loaded.installedPackage;
495
516
  delete loaded.failureStreak;
496
517
  return [loaded];
497
518
  })
@@ -581,7 +602,27 @@ export class RoutineManager {
581
602
  .sort((a, b) => (b.finishedAt ?? b.createdAt) - (a.finishedAt ?? a.createdAt) || b.createdAt - a.createdAt);
582
603
  const success = outcomes.findIndex(run => run.status === "completed");
583
604
  const failures = success < 0 ? outcomes.length : success;
584
- return { ...cloneRoutine(routine), ...(failures ? { failureStreak: failures } : {}) };
605
+ const { installedPackage: _installedPackage, ...visible } = cloneRoutine(routine);
606
+ return { ...visible, ...(failures ? { failureStreak: failures } : {}) };
607
+ }
608
+ /** Routines added from the organization's library, with their stamps. */
609
+ packageStamps() {
610
+ return this.routines.flatMap((routine) => routine.installedPackage
611
+ ? [{ routineId: routine.id, botId: routine.botId, enabled: routine.enabled, stamp: structuredClone(routine.installedPackage) }]
612
+ : []);
613
+ }
614
+ /** Record which organization install a just-created routine belongs to. */
615
+ stampInstalledPackage(id, stamp) {
616
+ const clean = loadInstalledPackage(stamp);
617
+ if (!clean)
618
+ throw new Error("Invalid routine package stamp");
619
+ if (!this.routines.some((routine) => routine.id === id))
620
+ return false;
621
+ this.commitMutation(() => {
622
+ const routine = this.routines.find((candidate) => candidate.id === id);
623
+ routine.installedPackage = clean;
624
+ });
625
+ return true;
585
626
  }
586
627
  /** Whether this computer should stay awake for routines: a run is in
587
628
  * flight, or an enabled routine is due within the horizon. The schedule
@@ -1063,6 +1104,27 @@ export class RoutineManager {
1063
1104
  }
1064
1105
  return cloneRun(run);
1065
1106
  }
1107
+ /** Clear every failure indicator at once: stamp seenAt on each unseen
1108
+ * failed/missed run in one committed save, then emit the updated runs so
1109
+ * connected clients drop their dots immediately. A failed save rolls the
1110
+ * stamps back so a retry still finds the unseen runs. */
1111
+ markAllSeen() {
1112
+ if (!this.runs.some((run) => !run.seenAt && isRoutineProblemRun(run)))
1113
+ return [];
1114
+ const stampAt = this.now();
1115
+ const updated = [];
1116
+ this.commitMutation(() => {
1117
+ for (const run of this.runs) {
1118
+ if (run.seenAt || !isRoutineProblemRun(run))
1119
+ continue;
1120
+ run.seenAt = stampAt;
1121
+ updated.push(run);
1122
+ }
1123
+ });
1124
+ for (const run of updated)
1125
+ this.emitRun(run);
1126
+ return updated.map(cloneRun);
1127
+ }
1066
1128
  get isTicking() { return this.ticking; }
1067
1129
  start() {
1068
1130
  if (this.timer)
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
  import { z } from "zod";
10
10
  import { writeFileAtomic } from "./atomic.js";
11
11
  import { DATA_DIR } from "./config.js";
12
+ import { renamePublishedTeam } from "./published-teams.js";
12
13
  export const SECTION_CONTEXT_MAX_BYTES = 24_000;
13
14
  export const SECTION_CONTEXTS_FILE = join(DATA_DIR, "section-contexts.json");
14
15
  const sectionContextRecordSchema = z.object({ text: z.string(), updatedAt: z.number().finite() });
@@ -94,6 +95,15 @@ export function changeEmptySection(name, nextName) {
94
95
  if (nextName !== null && context)
95
96
  data.contexts[nextName] = context;
96
97
  saveFile(data);
98
+ // Every team rename and removal funnels through here, so the team's
99
+ // published package id and keys follow it (published-teams.ts). The team
100
+ // itself is already renamed; a failure here costs only key stability.
101
+ try {
102
+ renamePublishedTeam(name, nextName);
103
+ }
104
+ catch (error) {
105
+ console.warn(`[teams] Could not move the shared-package record for this team: ${error.message}`);
106
+ }
97
107
  }
98
108
  export function readSectionContext(section) {
99
109
  const record = loadFile().contexts[sectionContextKey(section)];
@@ -34,14 +34,11 @@ import { DATA_DIR } from "./config.js";
34
34
  import { redactSecretsInText } from "./redact.js";
35
35
  import { LEARN_SOURCE_PREFIX } from "./skill-learn.js";
36
36
  import { workspaceDir } from "./workspace.js";
37
- /** Spec rule: lowercase alphanumerics with single hyphens, 1-64 chars,
38
- * folder name must equal it. The regex IS the traversal gate — no dots, no
39
- * slashes, no way to name a skill "..". */
40
- const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
41
- export const SKILL_NAME_MAX = 64;
42
- export const DESCRIPTION_MAX = 1024;
43
- /** One SKILL.md may be at most this large; the spec recommends <5k tokens. */
44
- export const SKILL_FILE_MAX_BYTES = 256 * 1024;
37
+ import { isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES } from "../shared/skill-md.js";
38
+ // SKILL.md parsing and the static scan live in shared/ so the package
39
+ // format (Admin and the renderer included) validates skills with the exact
40
+ // same rules; re-exported here under their historical path.
41
+ export { DESCRIPTION_MAX, isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES, SKILL_NAME_MAX, } from "../shared/skill-md.js";
45
42
  /** Index budget: name+description lines only, ~100 tokens per skill. */
46
43
  export const INDEX_MAX_SKILLS = 30;
47
44
  export const INDEX_MAX_BYTES = 4_000;
@@ -51,58 +48,6 @@ export const STAGED_GIST_MAX = 240;
51
48
  /** Learned skills are duplicated onto their durable review card. Keep that
52
49
  * exact review payload bounded while leaving fetched skill imports unchanged. */
53
50
  export const STAGED_SKILL_FILE_MAX_BYTES = 32 * 1024;
54
- export function isSkillName(name) {
55
- return name.length >= 1 && name.length <= SKILL_NAME_MAX && SKILL_NAME.test(name);
56
- }
57
- /** Minimal frontmatter reader for the two required keys plus the two we
58
- * display. Deliberately not a YAML engine: values are single-line strings in
59
- * every skill the spec's own examples show, and a parser that cannot
60
- * evaluate anchors or tags cannot be surprised by them. */
61
- export function parseSkillMd(raw) {
62
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
63
- if (!match)
64
- return { error: "SKILL.md has no YAML frontmatter (--- block) at the top" };
65
- const fields = {};
66
- for (const line of match[1].split(/\r?\n/)) {
67
- const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
68
- if (!kv)
69
- continue;
70
- fields[kv[1].toLowerCase()] = kv[2].replace(/^["']|["']$/g, "").trim();
71
- }
72
- const name = fields.name ?? "";
73
- const description = fields.description ?? "";
74
- if (!isSkillName(name)) {
75
- return { error: `frontmatter name ${JSON.stringify(name)} is not a valid skill name (lowercase, hyphens, max ${SKILL_NAME_MAX})` };
76
- }
77
- if (!description || description.length > DESCRIPTION_MAX) {
78
- return { error: `frontmatter description is required and must be at most ${DESCRIPTION_MAX} characters` };
79
- }
80
- return {
81
- name,
82
- description,
83
- license: fields.license || undefined,
84
- compatibility: fields.compatibility || undefined,
85
- body: match[2] ?? "",
86
- };
87
- }
88
- /** Static red flags before a human review. Presence is a warning shown in
89
- * the review screen, never a silent rejection — the reviewer decides. These
90
- * are the three patterns the public registry audits actually caught. */
91
- export function scanSkillText(raw) {
92
- const warnings = [];
93
- if (/[A-Za-z0-9+/]{120,}={0,2}/.test(raw)) {
94
- warnings.push("contains a long base64-looking blob — a common wrapper for hidden instructions or payloads");
95
- }
96
- if (/\b(curl|wget)\b[^\n]{0,200}\|\s*(ba|z|da)?sh\b/.test(raw)) {
97
- warnings.push("pipes a download straight into a shell (curl|sh) — never enable without understanding why");
98
- }
99
- // zero-width and bidi-control characters hide text from the reviewer while
100
- // the model still reads it — the invisible-instruction trick
101
- if (/[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/.test(raw)) {
102
- warnings.push("contains invisible Unicode characters (zero-width or bidi controls) — text you cannot see");
103
- }
104
- return warnings;
105
- }
106
51
  const skillManifestEntrySchema = z.object({
107
52
  description: z.string(),
108
53
  enabled: z.boolean(),
@@ -115,6 +60,13 @@ const skillManifestEntrySchema = z.object({
115
60
  skippedFiles: z.array(z.string()),
116
61
  appliedStageId: z.string().optional(),
117
62
  storageRevision: z.string().regex(/^[a-f0-9]{64}$/).optional(),
63
+ package: z.object({
64
+ installId: z.string().regex(/^[a-f0-9]{32}$/),
65
+ key: z.string().min(1).max(64),
66
+ release: z.string().min(1).max(40),
67
+ r: z.string().regex(/^[a-f0-9]{64}$/),
68
+ w: z.string().regex(/^[a-f0-9]{64}$/),
69
+ }).optional(),
118
70
  });
119
71
  const skillManifestSchema = z.record(z.string(), skillManifestEntrySchema);
120
72
  const managedLinksSchema = z.array(z.string());
@@ -498,7 +450,7 @@ function skillContentMatches(botId, name, entry) {
498
450
  }
499
451
  }
500
452
  function skillListing(botId, name, entry) {
501
- const { appliedStageId, storageRevision: _storageRevision, ...visible } = entry;
453
+ const { appliedStageId, storageRevision: _storageRevision, package: _package, ...visible } = entry;
502
454
  const intact = skillContentMatches(botId, name, entry);
503
455
  return {
504
456
  name,
@@ -563,6 +515,26 @@ export function installSkill(botId, source, files) {
563
515
  return prepared;
564
516
  return installPreparedSkill(botId, source, prepared, { enabled: false });
565
517
  }
518
+ /** The organization library's path: a skill the organization's Admin
519
+ * published arrives switched ON (the Admin is the review), with the install
520
+ * stamp that lets the library find it again. `source` is chosen by the
521
+ * caller (`org:<ref>@<release>`), never read from the package. */
522
+ export function installOrgSkill(botId, source, skillMd, stamp) {
523
+ if (!source.startsWith("org:"))
524
+ return { error: "organization skills need an org: source" };
525
+ const prepared = preparedSkillFiles([{ path: "SKILL.md", content: skillMd }]);
526
+ if ("error" in prepared)
527
+ return prepared;
528
+ if (prepared.parsed.name !== stamp.key)
529
+ return { error: `the skill is named "${prepared.parsed.name}", not "${stamp.key}"` };
530
+ return installPreparedSkill(botId, source, prepared, { enabled: true, package: { ...stamp } });
531
+ }
532
+ /** A bot's skills that an organization install added, with their stamps. */
533
+ export function skillPackageStamps(botId) {
534
+ return Object.entries(readManifest(botId)).flatMap(([name, entry]) => entry.package
535
+ ? [{ name, enabled: entry.enabled, stamp: { ...entry.package } }]
536
+ : []);
537
+ }
566
538
  export function setSkillEnabled(botId, name, enabled) {
567
539
  if (!isSkillName(name))
568
540
  return { error: "invalid skill name" };
@@ -936,6 +908,7 @@ function installPreparedSkill(botId, source, prepared, options) {
936
908
  warnings: prepared.warnings,
937
909
  skippedFiles: prepared.skippedFiles,
938
910
  appliedStageId: options.appliedStageId,
911
+ ...(options.package ? { package: options.package } : {}),
939
912
  };
940
913
  const root = ensureSkillsRoot(botId);
941
914
  if (!root)